1/** 2 * @fileoverview A rule to control the use of single variable declarations. 3 * @author Ian Christian Myers 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Rule Definition 10//------------------------------------------------------------------------------ 11 12module.exports = { 13 meta: { 14 type: "suggestion", 15 16 docs: { 17 description: "enforce variables to be declared either together or separately in functions", 18 category: "Stylistic Issues", 19 recommended: false, 20 url: "https://eslint.org/docs/rules/one-var" 21 }, 22 23 fixable: "code", 24 25 schema: [ 26 { 27 oneOf: [ 28 { 29 enum: ["always", "never", "consecutive"] 30 }, 31 { 32 type: "object", 33 properties: { 34 separateRequires: { 35 type: "boolean" 36 }, 37 var: { 38 enum: ["always", "never", "consecutive"] 39 }, 40 let: { 41 enum: ["always", "never", "consecutive"] 42 }, 43 const: { 44 enum: ["always", "never", "consecutive"] 45 } 46 }, 47 additionalProperties: false 48 }, 49 { 50 type: "object", 51 properties: { 52 initialized: { 53 enum: ["always", "never", "consecutive"] 54 }, 55 uninitialized: { 56 enum: ["always", "never", "consecutive"] 57 } 58 }, 59 additionalProperties: false 60 } 61 ] 62 } 63 ], 64 65 messages: { 66 combineUninitialized: "Combine this with the previous '{{type}}' statement with uninitialized variables.", 67 combineInitialized: "Combine this with the previous '{{type}}' statement with initialized variables.", 68 splitUninitialized: "Split uninitialized '{{type}}' declarations into multiple statements.", 69 splitInitialized: "Split initialized '{{type}}' declarations into multiple statements.", 70 splitRequires: "Split requires to be separated into a single block.", 71 combine: "Combine this with the previous '{{type}}' statement.", 72 split: "Split '{{type}}' declarations into multiple statements." 73 } 74 }, 75 76 create(context) { 77 const MODE_ALWAYS = "always"; 78 const MODE_NEVER = "never"; 79 const MODE_CONSECUTIVE = "consecutive"; 80 const mode = context.options[0] || MODE_ALWAYS; 81 82 const options = {}; 83 84 if (typeof mode === "string") { // simple options configuration with just a string 85 options.var = { uninitialized: mode, initialized: mode }; 86 options.let = { uninitialized: mode, initialized: mode }; 87 options.const = { uninitialized: mode, initialized: mode }; 88 } else if (typeof mode === "object") { // options configuration is an object 89 options.separateRequires = !!mode.separateRequires; 90 options.var = { uninitialized: mode.var, initialized: mode.var }; 91 options.let = { uninitialized: mode.let, initialized: mode.let }; 92 options.const = { uninitialized: mode.const, initialized: mode.const }; 93 if (Object.prototype.hasOwnProperty.call(mode, "uninitialized")) { 94 options.var.uninitialized = mode.uninitialized; 95 options.let.uninitialized = mode.uninitialized; 96 options.const.uninitialized = mode.uninitialized; 97 } 98 if (Object.prototype.hasOwnProperty.call(mode, "initialized")) { 99 options.var.initialized = mode.initialized; 100 options.let.initialized = mode.initialized; 101 options.const.initialized = mode.initialized; 102 } 103 } 104 105 const sourceCode = context.getSourceCode(); 106 107 //-------------------------------------------------------------------------- 108 // Helpers 109 //-------------------------------------------------------------------------- 110 111 const functionStack = []; 112 const blockStack = []; 113 114 /** 115 * Increments the blockStack counter. 116 * @returns {void} 117 * @private 118 */ 119 function startBlock() { 120 blockStack.push({ 121 let: { initialized: false, uninitialized: false }, 122 const: { initialized: false, uninitialized: false } 123 }); 124 } 125 126 /** 127 * Increments the functionStack counter. 128 * @returns {void} 129 * @private 130 */ 131 function startFunction() { 132 functionStack.push({ initialized: false, uninitialized: false }); 133 startBlock(); 134 } 135 136 /** 137 * Decrements the blockStack counter. 138 * @returns {void} 139 * @private 140 */ 141 function endBlock() { 142 blockStack.pop(); 143 } 144 145 /** 146 * Decrements the functionStack counter. 147 * @returns {void} 148 * @private 149 */ 150 function endFunction() { 151 functionStack.pop(); 152 endBlock(); 153 } 154 155 /** 156 * Check if a variable declaration is a require. 157 * @param {ASTNode} decl variable declaration Node 158 * @returns {bool} if decl is a require, return true; else return false. 159 * @private 160 */ 161 function isRequire(decl) { 162 return decl.init && decl.init.type === "CallExpression" && decl.init.callee.name === "require"; 163 } 164 165 /** 166 * Records whether initialized/uninitialized/required variables are defined in current scope. 167 * @param {string} statementType node.kind, one of: "var", "let", or "const" 168 * @param {ASTNode[]} declarations List of declarations 169 * @param {Object} currentScope The scope being investigated 170 * @returns {void} 171 * @private 172 */ 173 function recordTypes(statementType, declarations, currentScope) { 174 for (let i = 0; i < declarations.length; i++) { 175 if (declarations[i].init === null) { 176 if (options[statementType] && options[statementType].uninitialized === MODE_ALWAYS) { 177 currentScope.uninitialized = true; 178 } 179 } else { 180 if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) { 181 if (options.separateRequires && isRequire(declarations[i])) { 182 currentScope.required = true; 183 } else { 184 currentScope.initialized = true; 185 } 186 } 187 } 188 } 189 } 190 191 /** 192 * Determines the current scope (function or block) 193 * @param {string} statementType node.kind, one of: "var", "let", or "const" 194 * @returns {Object} The scope associated with statementType 195 */ 196 function getCurrentScope(statementType) { 197 let currentScope; 198 199 if (statementType === "var") { 200 currentScope = functionStack[functionStack.length - 1]; 201 } else if (statementType === "let") { 202 currentScope = blockStack[blockStack.length - 1].let; 203 } else if (statementType === "const") { 204 currentScope = blockStack[blockStack.length - 1].const; 205 } 206 return currentScope; 207 } 208 209 /** 210 * Counts the number of initialized and uninitialized declarations in a list of declarations 211 * @param {ASTNode[]} declarations List of declarations 212 * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations 213 * @private 214 */ 215 function countDeclarations(declarations) { 216 const counts = { uninitialized: 0, initialized: 0 }; 217 218 for (let i = 0; i < declarations.length; i++) { 219 if (declarations[i].init === null) { 220 counts.uninitialized++; 221 } else { 222 counts.initialized++; 223 } 224 } 225 return counts; 226 } 227 228 /** 229 * Determines if there is more than one var statement in the current scope. 230 * @param {string} statementType node.kind, one of: "var", "let", or "const" 231 * @param {ASTNode[]} declarations List of declarations 232 * @returns {boolean} Returns true if it is the first var declaration, false if not. 233 * @private 234 */ 235 function hasOnlyOneStatement(statementType, declarations) { 236 237 const declarationCounts = countDeclarations(declarations); 238 const currentOptions = options[statementType] || {}; 239 const currentScope = getCurrentScope(statementType); 240 const hasRequires = declarations.some(isRequire); 241 242 if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) { 243 if (currentScope.uninitialized || currentScope.initialized) { 244 if (!hasRequires) { 245 return false; 246 } 247 } 248 } 249 250 if (declarationCounts.uninitialized > 0) { 251 if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) { 252 return false; 253 } 254 } 255 if (declarationCounts.initialized > 0) { 256 if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) { 257 if (!hasRequires) { 258 return false; 259 } 260 } 261 } 262 if (currentScope.required && hasRequires) { 263 return false; 264 } 265 recordTypes(statementType, declarations, currentScope); 266 return true; 267 } 268 269 /** 270 * Fixer to join VariableDeclaration's into a single declaration 271 * @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join 272 * @returns {Function} The fixer function 273 */ 274 function joinDeclarations(declarations) { 275 const declaration = declarations[0]; 276 const body = Array.isArray(declaration.parent.parent.body) ? declaration.parent.parent.body : []; 277 const currentIndex = body.findIndex(node => node.range[0] === declaration.parent.range[0]); 278 const previousNode = body[currentIndex - 1]; 279 280 return fixer => { 281 const type = sourceCode.getTokenBefore(declaration); 282 const prevSemi = sourceCode.getTokenBefore(type); 283 const res = []; 284 285 if (previousNode && previousNode.kind === sourceCode.getText(type)) { 286 if (prevSemi.value === ";") { 287 res.push(fixer.replaceText(prevSemi, ",")); 288 } else { 289 res.push(fixer.insertTextAfter(prevSemi, ",")); 290 } 291 res.push(fixer.replaceText(type, "")); 292 } 293 294 return res; 295 }; 296 } 297 298 /** 299 * Fixer to split a VariableDeclaration into individual declarations 300 * @param {VariableDeclaration} declaration The `VariableDeclaration` to split 301 * @returns {Function} The fixer function 302 */ 303 function splitDeclarations(declaration) { 304 return fixer => declaration.declarations.map(declarator => { 305 const tokenAfterDeclarator = sourceCode.getTokenAfter(declarator); 306 307 if (tokenAfterDeclarator === null) { 308 return null; 309 } 310 311 const afterComma = sourceCode.getTokenAfter(tokenAfterDeclarator, { includeComments: true }); 312 313 if (tokenAfterDeclarator.value !== ",") { 314 return null; 315 } 316 317 /* 318 * `var x,y` 319 * tokenAfterDeclarator ^^ afterComma 320 */ 321 if (afterComma.range[0] === tokenAfterDeclarator.range[1]) { 322 return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind} `); 323 } 324 325 /* 326 * `var x, 327 * tokenAfterDeclarator ^ 328 * y` 329 * ^ afterComma 330 */ 331 if ( 332 afterComma.loc.start.line > tokenAfterDeclarator.loc.end.line || 333 afterComma.type === "Line" || 334 afterComma.type === "Block" 335 ) { 336 let lastComment = afterComma; 337 338 while (lastComment.type === "Line" || lastComment.type === "Block") { 339 lastComment = sourceCode.getTokenAfter(lastComment, { includeComments: true }); 340 } 341 342 return fixer.replaceTextRange( 343 [tokenAfterDeclarator.range[0], lastComment.range[0]], 344 `;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${declaration.kind} ` 345 ); 346 } 347 348 return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind}`); 349 }).filter(x => x); 350 } 351 352 /** 353 * Checks a given VariableDeclaration node for errors. 354 * @param {ASTNode} node The VariableDeclaration node to check 355 * @returns {void} 356 * @private 357 */ 358 function checkVariableDeclaration(node) { 359 const parent = node.parent; 360 const type = node.kind; 361 362 if (!options[type]) { 363 return; 364 } 365 366 const declarations = node.declarations; 367 const declarationCounts = countDeclarations(declarations); 368 const mixedRequires = declarations.some(isRequire) && !declarations.every(isRequire); 369 370 if (options[type].initialized === MODE_ALWAYS) { 371 if (options.separateRequires && mixedRequires) { 372 context.report({ 373 node, 374 messageId: "splitRequires" 375 }); 376 } 377 } 378 379 // consecutive 380 const nodeIndex = (parent.body && parent.body.length > 0 && parent.body.indexOf(node)) || 0; 381 382 if (nodeIndex > 0) { 383 const previousNode = parent.body[nodeIndex - 1]; 384 const isPreviousNodeDeclaration = previousNode.type === "VariableDeclaration"; 385 const declarationsWithPrevious = declarations.concat(previousNode.declarations || []); 386 387 if ( 388 isPreviousNodeDeclaration && 389 previousNode.kind === type && 390 !(declarationsWithPrevious.some(isRequire) && !declarationsWithPrevious.every(isRequire)) 391 ) { 392 const previousDeclCounts = countDeclarations(previousNode.declarations); 393 394 if (options[type].initialized === MODE_CONSECUTIVE && options[type].uninitialized === MODE_CONSECUTIVE) { 395 context.report({ 396 node, 397 messageId: "combine", 398 data: { 399 type 400 }, 401 fix: joinDeclarations(declarations) 402 }); 403 } else if (options[type].initialized === MODE_CONSECUTIVE && declarationCounts.initialized > 0 && previousDeclCounts.initialized > 0) { 404 context.report({ 405 node, 406 messageId: "combineInitialized", 407 data: { 408 type 409 }, 410 fix: joinDeclarations(declarations) 411 }); 412 } else if (options[type].uninitialized === MODE_CONSECUTIVE && 413 declarationCounts.uninitialized > 0 && 414 previousDeclCounts.uninitialized > 0) { 415 context.report({ 416 node, 417 messageId: "combineUninitialized", 418 data: { 419 type 420 }, 421 fix: joinDeclarations(declarations) 422 }); 423 } 424 } 425 } 426 427 // always 428 if (!hasOnlyOneStatement(type, declarations)) { 429 if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) { 430 context.report({ 431 node, 432 messageId: "combine", 433 data: { 434 type 435 }, 436 fix: joinDeclarations(declarations) 437 }); 438 } else { 439 if (options[type].initialized === MODE_ALWAYS && declarationCounts.initialized > 0) { 440 context.report({ 441 node, 442 messageId: "combineInitialized", 443 data: { 444 type 445 }, 446 fix: joinDeclarations(declarations) 447 }); 448 } 449 if (options[type].uninitialized === MODE_ALWAYS && declarationCounts.uninitialized > 0) { 450 if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) { 451 return; 452 } 453 context.report({ 454 node, 455 messageId: "combineUninitialized", 456 data: { 457 type 458 }, 459 fix: joinDeclarations(declarations) 460 }); 461 } 462 } 463 } 464 465 // never 466 if (parent.type !== "ForStatement" || parent.init !== node) { 467 const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized; 468 469 if (totalDeclarations > 1) { 470 if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) { 471 472 // both initialized and uninitialized 473 context.report({ 474 node, 475 messageId: "split", 476 data: { 477 type 478 }, 479 fix: splitDeclarations(node) 480 }); 481 } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) { 482 483 // initialized 484 context.report({ 485 node, 486 messageId: "splitInitialized", 487 data: { 488 type 489 }, 490 fix: splitDeclarations(node) 491 }); 492 } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) { 493 494 // uninitialized 495 context.report({ 496 node, 497 messageId: "splitUninitialized", 498 data: { 499 type 500 }, 501 fix: splitDeclarations(node) 502 }); 503 } 504 } 505 } 506 } 507 508 //-------------------------------------------------------------------------- 509 // Public API 510 //-------------------------------------------------------------------------- 511 512 return { 513 Program: startFunction, 514 FunctionDeclaration: startFunction, 515 FunctionExpression: startFunction, 516 ArrowFunctionExpression: startFunction, 517 BlockStatement: startBlock, 518 ForStatement: startBlock, 519 ForInStatement: startBlock, 520 ForOfStatement: startBlock, 521 SwitchStatement: startBlock, 522 VariableDeclaration: checkVariableDeclaration, 523 "ForStatement:exit": endBlock, 524 "ForOfStatement:exit": endBlock, 525 "ForInStatement:exit": endBlock, 526 "SwitchStatement:exit": endBlock, 527 "BlockStatement:exit": endBlock, 528 "Program:exit": endFunction, 529 "FunctionDeclaration:exit": endFunction, 530 "FunctionExpression:exit": endFunction, 531 "ArrowFunctionExpression:exit": endFunction 532 }; 533 534 } 535}; 536