/** * @fileoverview A rule to control the use of single variable declarations. * @author Ian Christian Myers */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: "enforce variables to be declared either together or separately in functions", category: "Stylistic Issues", recommended: false, url: "https://eslint.org/docs/rules/one-var" }, schema: [ { oneOf: [ { enum: ["always", "never", "consecutive"] }, { type: "object", properties: { separateRequires: { type: "boolean" }, var: { enum: ["always", "never", "consecutive"] }, let: { enum: ["always", "never", "consecutive"] }, const: { enum: ["always", "never", "consecutive"] } }, additionalProperties: false }, { type: "object", properties: { initialized: { enum: ["always", "never", "consecutive"] }, uninitialized: { enum: ["always", "never", "consecutive"] } }, additionalProperties: false } ] } ] }, create(context) { const MODE_ALWAYS = "always"; const MODE_NEVER = "never"; const MODE_CONSECUTIVE = "consecutive"; const mode = context.options[0] || MODE_ALWAYS; const options = {}; if (typeof mode === "string") { // simple options configuration with just a string options.var = { uninitialized: mode, initialized: mode }; options.let = { uninitialized: mode, initialized: mode }; options.const = { uninitialized: mode, initialized: mode }; } else if (typeof mode === "object") { // options configuration is an object if (mode.hasOwnProperty("separateRequires")) { options.separateRequires = !!mode.separateRequires; } if (mode.hasOwnProperty("var")) { options.var = { uninitialized: mode.var, initialized: mode.var }; } if (mode.hasOwnProperty("let")) { options.let = { uninitialized: mode.let, initialized: mode.let }; } if (mode.hasOwnProperty("const")) { options.const = { uninitialized: mode.const, initialized: mode.const }; } if (mode.hasOwnProperty("uninitialized")) { if (!options.var) { options.var = {}; } if (!options.let) { options.let = {}; } if (!options.const) { options.const = {}; } options.var.uninitialized = mode.uninitialized; options.let.uninitialized = mode.uninitialized; options.const.uninitialized = mode.uninitialized; } if (mode.hasOwnProperty("initialized")) { if (!options.var) { options.var = {}; } if (!options.let) { options.let = {}; } if (!options.const) { options.const = {}; } options.var.initialized = mode.initialized; options.let.initialized = mode.initialized; options.const.initialized = mode.initialized; } } //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- const functionStack = []; const blockStack = []; /** * Increments the blockStack counter. * @returns {void} * @private */ function startBlock() { blockStack.push({ let: { initialized: false, uninitialized: false }, const: { initialized: false, uninitialized: false } }); } /** * Increments the functionStack counter. * @returns {void} * @private */ function startFunction() { functionStack.push({ initialized: false, uninitialized: false }); startBlock(); } /** * Decrements the blockStack counter. * @returns {void} * @private */ function endBlock() { blockStack.pop(); } /** * Decrements the functionStack counter. * @returns {void} * @private */ function endFunction() { functionStack.pop(); endBlock(); } /** * Check if a variable declaration is a require. * @param {ASTNode} decl variable declaration Node * @returns {bool} if decl is a require, return true; else return false. * @private */ function isRequire(decl) { return decl.init && decl.init.type === "CallExpression" && decl.init.callee.name === "require"; } /** * Records whether initialized/uninitialized/required variables are defined in current scope. * @param {string} statementType node.kind, one of: "var", "let", or "const" * @param {ASTNode[]} declarations List of declarations * @param {Object} currentScope The scope being investigated * @returns {void} * @private */ function recordTypes(statementType, declarations, currentScope) { for (let i = 0; i < declarations.length; i++) { if (declarations[i].init === null) { if (options[statementType] && options[statementType].uninitialized === MODE_ALWAYS) { currentScope.uninitialized = true; } } else { if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) { if (options.separateRequires && isRequire(declarations[i])) { currentScope.required = true; } else { currentScope.initialized = true; } } } } } /** * Determines the current scope (function or block) * @param {string} statementType node.kind, one of: "var", "let", or "const" * @returns {Object} The scope associated with statementType */ function getCurrentScope(statementType) { let currentScope; if (statementType === "var") { currentScope = functionStack[functionStack.length - 1]; } else if (statementType === "let") { currentScope = blockStack[blockStack.length - 1].let; } else if (statementType === "const") { currentScope = blockStack[blockStack.length - 1].const; } return currentScope; } /** * Counts the number of initialized and uninitialized declarations in a list of declarations * @param {ASTNode[]} declarations List of declarations * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations * @private */ function countDeclarations(declarations) { const counts = { uninitialized: 0, initialized: 0 }; for (let i = 0; i < declarations.length; i++) { if (declarations[i].init === null) { counts.uninitialized++; } else { counts.initialized++; } } return counts; } /** * Determines if there is more than one var statement in the current scope. * @param {string} statementType node.kind, one of: "var", "let", or "const" * @param {ASTNode[]} declarations List of declarations * @returns {boolean} Returns true if it is the first var declaration, false if not. * @private */ function hasOnlyOneStatement(statementType, declarations) { const declarationCounts = countDeclarations(declarations); const currentOptions = options[statementType] || {}; const currentScope = getCurrentScope(statementType); const hasRequires = declarations.some(isRequire); if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) { if (currentScope.uninitialized || currentScope.initialized) { return false; } } if (declarationCounts.uninitialized > 0) { if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) { return false; } } if (declarationCounts.initialized > 0) { if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) { return false; } } if (currentScope.required && hasRequires) { return false; } recordTypes(statementType, declarations, currentScope); return true; } /** * Checks a given VariableDeclaration node for errors. * @param {ASTNode} node The VariableDeclaration node to check * @returns {void} * @private */ function checkVariableDeclaration(node) { const parent = node.parent; const type = node.kind; if (!options[type]) { return; } const declarations = node.declarations; const declarationCounts = countDeclarations(declarations); const mixedRequires = declarations.some(isRequire) && !declarations.every(isRequire); if (options[type].initialized === MODE_ALWAYS) { if (options.separateRequires && mixedRequires) { context.report({ node, message: "Split requires to be separated into a single block." }); } } // consecutive const nodeIndex = (parent.body && parent.body.length > 0 && parent.body.indexOf(node)) || 0; if (nodeIndex > 0) { const previousNode = parent.body[nodeIndex - 1]; const isPreviousNodeDeclaration = previousNode.type === "VariableDeclaration"; if (isPreviousNodeDeclaration && previousNode.kind === type) { const previousDeclCounts = countDeclarations(previousNode.declarations); if (options[type].initialized === MODE_CONSECUTIVE && options[type].uninitialized === MODE_CONSECUTIVE) { context.report({ node, message: "Combine this with the previous '{{type}}' statement.", data: { type } }); } else if (options[type].initialized === MODE_CONSECUTIVE && declarationCounts.initialized > 0 && previousDeclCounts.initialized > 0) { context.report({ node, message: "Combine this with the previous '{{type}}' statement with initialized variables.", data: { type } }); } else if (options[type].uninitialized === MODE_CONSECUTIVE && declarationCounts.uninitialized > 0 && previousDeclCounts.uninitialized > 0) { context.report({ node, message: "Combine this with the previous '{{type}}' statement with uninitialized variables.", data: { type } }); } } } // always if (!hasOnlyOneStatement(type, declarations)) { if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) { context.report({ node, message: "Combine this with the previous '{{type}}' statement.", data: { type } }); } else { if (options[type].initialized === MODE_ALWAYS && declarationCounts.initialized > 0) { context.report({ node, message: "Combine this with the previous '{{type}}' statement with initialized variables.", data: { type } }); } if (options[type].uninitialized === MODE_ALWAYS && declarationCounts.uninitialized > 0) { if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) { return; } context.report({ node, message: "Combine this with the previous '{{type}}' statement with uninitialized variables.", data: { type } }); } } } // never if (parent.type !== "ForStatement" || parent.init !== node) { const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized; if (totalDeclarations > 1) { if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) { // both initialized and uninitialized context.report({ node, message: "Split '{{type}}' declarations into multiple statements.", data: { type } }); } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) { // initialized context.report({ node, message: "Split initialized '{{type}}' declarations into multiple statements.", data: { type } }); } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) { // uninitialized context.report({ node, message: "Split uninitialized '{{type}}' declarations into multiple statements.", data: { type } }); } } } } //-------------------------------------------------------------------------- // Public API //-------------------------------------------------------------------------- return { Program: startFunction, FunctionDeclaration: startFunction, FunctionExpression: startFunction, ArrowFunctionExpression: startFunction, BlockStatement: startBlock, ForStatement: startBlock, ForInStatement: startBlock, ForOfStatement: startBlock, SwitchStatement: startBlock, VariableDeclaration: checkVariableDeclaration, "ForStatement:exit": endBlock, "ForOfStatement:exit": endBlock, "ForInStatement:exit": endBlock, "SwitchStatement:exit": endBlock, "BlockStatement:exit": endBlock, "Program:exit": endFunction, "FunctionDeclaration:exit": endFunction, "FunctionExpression:exit": endFunction, "ArrowFunctionExpression:exit": endFunction }; } };