2 * @fileoverview A rule to control the use of single variable declarations.
3 * @author Ian Christian Myers
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
17 description: "enforce variables to be declared either together or separately in functions",
18 category: "Stylistic Issues",
20 url: "https://eslint.org/docs/rules/one-var"
29 enum: ["always", "never", "consecutive"]
38 enum: ["always", "never", "consecutive"]
41 enum: ["always", "never", "consecutive"]
44 enum: ["always", "never", "consecutive"]
47 additionalProperties: false
53 enum: ["always", "never", "consecutive"]
56 enum: ["always", "never", "consecutive"]
59 additionalProperties: false
67 const MODE_ALWAYS = "always";
68 const MODE_NEVER = "never";
69 const MODE_CONSECUTIVE = "consecutive";
70 const mode = context.options[0] || MODE_ALWAYS;
74 if (typeof mode === "string") { // simple options configuration with just a string
75 options.var = { uninitialized: mode, initialized: mode };
76 options.let = { uninitialized: mode, initialized: mode };
77 options.const = { uninitialized: mode, initialized: mode };
78 } else if (typeof mode === "object") { // options configuration is an object
79 options.separateRequires = !!mode.separateRequires;
80 options.var = { uninitialized: mode.var, initialized: mode.var };
81 options.let = { uninitialized: mode.let, initialized: mode.let };
82 options.const = { uninitialized: mode.const, initialized: mode.const };
83 if (Object.prototype.hasOwnProperty.call(mode, "uninitialized")) {
84 options.var.uninitialized = mode.uninitialized;
85 options.let.uninitialized = mode.uninitialized;
86 options.const.uninitialized = mode.uninitialized;
88 if (Object.prototype.hasOwnProperty.call(mode, "initialized")) {
89 options.var.initialized = mode.initialized;
90 options.let.initialized = mode.initialized;
91 options.const.initialized = mode.initialized;
95 const sourceCode = context.getSourceCode();
97 //--------------------------------------------------------------------------
99 //--------------------------------------------------------------------------
101 const functionStack = [];
102 const blockStack = [];
105 * Increments the blockStack counter.
109 function startBlock() {
111 let: { initialized: false, uninitialized: false },
112 const: { initialized: false, uninitialized: false }
117 * Increments the functionStack counter.
121 function startFunction() {
122 functionStack.push({ initialized: false, uninitialized: false });
127 * Decrements the blockStack counter.
131 function endBlock() {
136 * Decrements the functionStack counter.
140 function endFunction() {
146 * Check if a variable declaration is a require.
147 * @param {ASTNode} decl variable declaration Node
148 * @returns {bool} if decl is a require, return true; else return false.
151 function isRequire(decl) {
152 return decl.init && decl.init.type === "CallExpression" && decl.init.callee.name === "require";
156 * Records whether initialized/uninitialized/required variables are defined in current scope.
157 * @param {string} statementType node.kind, one of: "var", "let", or "const"
158 * @param {ASTNode[]} declarations List of declarations
159 * @param {Object} currentScope The scope being investigated
163 function recordTypes(statementType, declarations, currentScope) {
164 for (let i = 0; i < declarations.length; i++) {
165 if (declarations[i].init === null) {
166 if (options[statementType] && options[statementType].uninitialized === MODE_ALWAYS) {
167 currentScope.uninitialized = true;
170 if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) {
171 if (options.separateRequires && isRequire(declarations[i])) {
172 currentScope.required = true;
174 currentScope.initialized = true;
182 * Determines the current scope (function or block)
183 * @param {string} statementType node.kind, one of: "var", "let", or "const"
184 * @returns {Object} The scope associated with statementType
186 function getCurrentScope(statementType) {
189 if (statementType === "var") {
190 currentScope = functionStack[functionStack.length - 1];
191 } else if (statementType === "let") {
192 currentScope = blockStack[blockStack.length - 1].let;
193 } else if (statementType === "const") {
194 currentScope = blockStack[blockStack.length - 1].const;
200 * Counts the number of initialized and uninitialized declarations in a list of declarations
201 * @param {ASTNode[]} declarations List of declarations
202 * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations
205 function countDeclarations(declarations) {
206 const counts = { uninitialized: 0, initialized: 0 };
208 for (let i = 0; i < declarations.length; i++) {
209 if (declarations[i].init === null) {
210 counts.uninitialized++;
212 counts.initialized++;
219 * Determines if there is more than one var statement in the current scope.
220 * @param {string} statementType node.kind, one of: "var", "let", or "const"
221 * @param {ASTNode[]} declarations List of declarations
222 * @returns {boolean} Returns true if it is the first var declaration, false if not.
225 function hasOnlyOneStatement(statementType, declarations) {
227 const declarationCounts = countDeclarations(declarations);
228 const currentOptions = options[statementType] || {};
229 const currentScope = getCurrentScope(statementType);
230 const hasRequires = declarations.some(isRequire);
232 if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) {
233 if (currentScope.uninitialized || currentScope.initialized) {
240 if (declarationCounts.uninitialized > 0) {
241 if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) {
245 if (declarationCounts.initialized > 0) {
246 if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) {
252 if (currentScope.required && hasRequires) {
255 recordTypes(statementType, declarations, currentScope);
260 * Fixer to join VariableDeclaration's into a single declaration
261 * @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join
262 * @returns {Function} The fixer function
264 function joinDeclarations(declarations) {
265 const declaration = declarations[0];
266 const body = Array.isArray(declaration.parent.parent.body) ? declaration.parent.parent.body : [];
267 const currentIndex = body.findIndex(node => node.range[0] === declaration.parent.range[0]);
268 const previousNode = body[currentIndex - 1];
271 const type = sourceCode.getTokenBefore(declaration);
272 const prevSemi = sourceCode.getTokenBefore(type);
275 if (previousNode && previousNode.kind === sourceCode.getText(type)) {
276 if (prevSemi.value === ";") {
277 res.push(fixer.replaceText(prevSemi, ","));
279 res.push(fixer.insertTextAfter(prevSemi, ","));
281 res.push(fixer.replaceText(type, ""));
289 * Fixer to split a VariableDeclaration into individual declarations
290 * @param {VariableDeclaration} declaration The `VariableDeclaration` to split
291 * @returns {Function} The fixer function
293 function splitDeclarations(declaration) {
294 return fixer => declaration.declarations.map(declarator => {
295 const tokenAfterDeclarator = sourceCode.getTokenAfter(declarator);
297 if (tokenAfterDeclarator === null) {
301 const afterComma = sourceCode.getTokenAfter(tokenAfterDeclarator, { includeComments: true });
303 if (tokenAfterDeclarator.value !== ",") {
309 * tokenAfterDeclarator ^^ afterComma
311 if (afterComma.range[0] === tokenAfterDeclarator.range[1]) {
312 return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind} `);
317 * tokenAfterDeclarator ^
322 afterComma.loc.start.line > tokenAfterDeclarator.loc.end.line ||
323 afterComma.type === "Line" ||
324 afterComma.type === "Block"
326 let lastComment = afterComma;
328 while (lastComment.type === "Line" || lastComment.type === "Block") {
329 lastComment = sourceCode.getTokenAfter(lastComment, { includeComments: true });
332 return fixer.replaceTextRange(
333 [tokenAfterDeclarator.range[0], lastComment.range[0]],
334 `;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${declaration.kind} `
338 return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind}`);
343 * Checks a given VariableDeclaration node for errors.
344 * @param {ASTNode} node The VariableDeclaration node to check
348 function checkVariableDeclaration(node) {
349 const parent = node.parent;
350 const type = node.kind;
352 if (!options[type]) {
356 const declarations = node.declarations;
357 const declarationCounts = countDeclarations(declarations);
358 const mixedRequires = declarations.some(isRequire) && !declarations.every(isRequire);
360 if (options[type].initialized === MODE_ALWAYS) {
361 if (options.separateRequires && mixedRequires) {
364 message: "Split requires to be separated into a single block."
370 const nodeIndex = (parent.body && parent.body.length > 0 && parent.body.indexOf(node)) || 0;
373 const previousNode = parent.body[nodeIndex - 1];
374 const isPreviousNodeDeclaration = previousNode.type === "VariableDeclaration";
375 const declarationsWithPrevious = declarations.concat(previousNode.declarations || []);
378 isPreviousNodeDeclaration &&
379 previousNode.kind === type &&
380 !(declarationsWithPrevious.some(isRequire) && !declarationsWithPrevious.every(isRequire))
382 const previousDeclCounts = countDeclarations(previousNode.declarations);
384 if (options[type].initialized === MODE_CONSECUTIVE && options[type].uninitialized === MODE_CONSECUTIVE) {
387 message: "Combine this with the previous '{{type}}' statement.",
391 fix: joinDeclarations(declarations)
393 } else if (options[type].initialized === MODE_CONSECUTIVE && declarationCounts.initialized > 0 && previousDeclCounts.initialized > 0) {
396 message: "Combine this with the previous '{{type}}' statement with initialized variables.",
400 fix: joinDeclarations(declarations)
402 } else if (options[type].uninitialized === MODE_CONSECUTIVE &&
403 declarationCounts.uninitialized > 0 &&
404 previousDeclCounts.uninitialized > 0) {
407 message: "Combine this with the previous '{{type}}' statement with uninitialized variables.",
411 fix: joinDeclarations(declarations)
418 if (!hasOnlyOneStatement(type, declarations)) {
419 if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) {
422 message: "Combine this with the previous '{{type}}' statement.",
426 fix: joinDeclarations(declarations)
429 if (options[type].initialized === MODE_ALWAYS && declarationCounts.initialized > 0) {
432 message: "Combine this with the previous '{{type}}' statement with initialized variables.",
436 fix: joinDeclarations(declarations)
439 if (options[type].uninitialized === MODE_ALWAYS && declarationCounts.uninitialized > 0) {
440 if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) {
445 message: "Combine this with the previous '{{type}}' statement with uninitialized variables.",
449 fix: joinDeclarations(declarations)
456 if (parent.type !== "ForStatement" || parent.init !== node) {
457 const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized;
459 if (totalDeclarations > 1) {
460 if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) {
462 // both initialized and uninitialized
465 message: "Split '{{type}}' declarations into multiple statements.",
469 fix: splitDeclarations(node)
471 } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) {
476 message: "Split initialized '{{type}}' declarations into multiple statements.",
480 fix: splitDeclarations(node)
482 } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) {
487 message: "Split uninitialized '{{type}}' declarations into multiple statements.",
491 fix: splitDeclarations(node)
498 //--------------------------------------------------------------------------
500 //--------------------------------------------------------------------------
503 Program: startFunction,
504 FunctionDeclaration: startFunction,
505 FunctionExpression: startFunction,
506 ArrowFunctionExpression: startFunction,
507 BlockStatement: startBlock,
508 ForStatement: startBlock,
509 ForInStatement: startBlock,
510 ForOfStatement: startBlock,
511 SwitchStatement: startBlock,
512 VariableDeclaration: checkVariableDeclaration,
513 "ForStatement:exit": endBlock,
514 "ForOfStatement:exit": endBlock,
515 "ForInStatement:exit": endBlock,
516 "SwitchStatement:exit": endBlock,
517 "BlockStatement:exit": endBlock,
518 "Program:exit": endFunction,
519 "FunctionDeclaration:exit": endFunction,
520 "FunctionExpression:exit": endFunction,
521 "ArrowFunctionExpression:exit": endFunction