| /** |
| * @fileoverview A rule to control the use of single variable declarations. |
| * @author Ian Christian Myers |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| 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" |
| }, |
| |
| fixable: "code", |
| |
| 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 |
| options.separateRequires = !!mode.separateRequires; |
| options.var = { uninitialized: mode.var, initialized: mode.var }; |
| options.let = { uninitialized: mode.let, initialized: mode.let }; |
| options.const = { uninitialized: mode.const, initialized: mode.const }; |
| if (Object.prototype.hasOwnProperty.call(mode, "uninitialized")) { |
| options.var.uninitialized = mode.uninitialized; |
| options.let.uninitialized = mode.uninitialized; |
| options.const.uninitialized = mode.uninitialized; |
| } |
| if (Object.prototype.hasOwnProperty.call(mode, "initialized")) { |
| options.var.initialized = mode.initialized; |
| options.let.initialized = mode.initialized; |
| options.const.initialized = mode.initialized; |
| } |
| } |
| |
| const sourceCode = context.getSourceCode(); |
| |
| //-------------------------------------------------------------------------- |
| // 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) { |
| if (!hasRequires) { |
| 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) { |
| if (!hasRequires) { |
| return false; |
| } |
| } |
| } |
| if (currentScope.required && hasRequires) { |
| return false; |
| } |
| recordTypes(statementType, declarations, currentScope); |
| return true; |
| } |
| |
| /** |
| * Fixer to join VariableDeclaration's into a single declaration |
| * @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join |
| * @returns {Function} The fixer function |
| */ |
| function joinDeclarations(declarations) { |
| const declaration = declarations[0]; |
| const body = Array.isArray(declaration.parent.parent.body) ? declaration.parent.parent.body : []; |
| const currentIndex = body.findIndex(node => node.range[0] === declaration.parent.range[0]); |
| const previousNode = body[currentIndex - 1]; |
| |
| return fixer => { |
| const type = sourceCode.getTokenBefore(declaration); |
| const prevSemi = sourceCode.getTokenBefore(type); |
| const res = []; |
| |
| if (previousNode && previousNode.kind === sourceCode.getText(type)) { |
| if (prevSemi.value === ";") { |
| res.push(fixer.replaceText(prevSemi, ",")); |
| } else { |
| res.push(fixer.insertTextAfter(prevSemi, ",")); |
| } |
| res.push(fixer.replaceText(type, "")); |
| } |
| |
| return res; |
| }; |
| } |
| |
| /** |
| * Fixer to split a VariableDeclaration into individual declarations |
| * @param {VariableDeclaration} declaration The `VariableDeclaration` to split |
| * @returns {Function} The fixer function |
| */ |
| function splitDeclarations(declaration) { |
| return fixer => declaration.declarations.map(declarator => { |
| const tokenAfterDeclarator = sourceCode.getTokenAfter(declarator); |
| |
| if (tokenAfterDeclarator === null) { |
| return null; |
| } |
| |
| const afterComma = sourceCode.getTokenAfter(tokenAfterDeclarator, { includeComments: true }); |
| |
| if (tokenAfterDeclarator.value !== ",") { |
| return null; |
| } |
| |
| /* |
| * `var x,y` |
| * tokenAfterDeclarator ^^ afterComma |
| */ |
| if (afterComma.range[0] === tokenAfterDeclarator.range[1]) { |
| return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind} `); |
| } |
| |
| /* |
| * `var x, |
| * tokenAfterDeclarator ^ |
| * y` |
| * ^ afterComma |
| */ |
| if ( |
| afterComma.loc.start.line > tokenAfterDeclarator.loc.end.line || |
| afterComma.type === "Line" || |
| afterComma.type === "Block" |
| ) { |
| let lastComment = afterComma; |
| |
| while (lastComment.type === "Line" || lastComment.type === "Block") { |
| lastComment = sourceCode.getTokenAfter(lastComment, { includeComments: true }); |
| } |
| |
| return fixer.replaceTextRange( |
| [tokenAfterDeclarator.range[0], lastComment.range[0]], |
| `;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${declaration.kind} ` |
| ); |
| } |
| |
| return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind}`); |
| }).filter(x => x); |
| } |
| |
| /** |
| * 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"; |
| const declarationsWithPrevious = declarations.concat(previousNode.declarations || []); |
| |
| if ( |
| isPreviousNodeDeclaration && |
| previousNode.kind === type && |
| !(declarationsWithPrevious.some(isRequire) && !declarationsWithPrevious.every(isRequire)) |
| ) { |
| 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 |
| }, |
| fix: joinDeclarations(declarations) |
| }); |
| } 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 |
| }, |
| fix: joinDeclarations(declarations) |
| }); |
| } 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 |
| }, |
| fix: joinDeclarations(declarations) |
| }); |
| } |
| } |
| } |
| |
| // 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 |
| }, |
| fix: joinDeclarations(declarations) |
| }); |
| } 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 |
| }, |
| fix: joinDeclarations(declarations) |
| }); |
| } |
| 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 |
| }, |
| fix: joinDeclarations(declarations) |
| }); |
| } |
| } |
| } |
| |
| // 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 |
| }, |
| fix: splitDeclarations(node) |
| }); |
| } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) { |
| |
| // initialized |
| context.report({ |
| node, |
| message: "Split initialized '{{type}}' declarations into multiple statements.", |
| data: { |
| type |
| }, |
| fix: splitDeclarations(node) |
| }); |
| } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) { |
| |
| // uninitialized |
| context.report({ |
| node, |
| message: "Split uninitialized '{{type}}' declarations into multiple statements.", |
| data: { |
| type |
| }, |
| fix: splitDeclarations(node) |
| }); |
| } |
| } |
| } |
| } |
| |
| //-------------------------------------------------------------------------- |
| // 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 |
| }; |
| |
| } |
| }; |