| /** |
| * @fileoverview A rule to control the use of single variable declarations. |
| * @author Ian Christian Myers |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const astUtils = require("./utils/ast-utils"); |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| /** |
| * Determines whether the given node is in a statement list. |
| * @param {ASTNode} node node to check |
| * @returns {boolean} `true` if the given node is in a statement list |
| */ |
| function isInStatementList(node) { |
| return astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type); |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| /** @type {import('../types').Rule.RuleModule} */ |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| docs: { |
| description: |
| "Enforce variables to be declared either together or separately in functions", |
| recommended: false, |
| frozen: true, |
| url: "https://eslint.org/docs/latest/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"], |
| }, |
| using: { |
| enum: ["always", "never", "consecutive"], |
| }, |
| awaitUsing: { |
| enum: ["always", "never", "consecutive"], |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| { |
| type: "object", |
| properties: { |
| initialized: { |
| enum: ["always", "never", "consecutive"], |
| }, |
| uninitialized: { |
| enum: ["always", "never", "consecutive"], |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| ], |
| }, |
| ], |
| |
| messages: { |
| combineUninitialized: |
| "Combine this with the previous '{{type}}' statement with uninitialized variables.", |
| combineInitialized: |
| "Combine this with the previous '{{type}}' statement with initialized variables.", |
| splitUninitialized: |
| "Split uninitialized '{{type}}' declarations into multiple statements.", |
| splitInitialized: |
| "Split initialized '{{type}}' declarations into multiple statements.", |
| splitRequires: |
| "Split requires to be separated into a single block.", |
| combine: "Combine this with the previous '{{type}}' statement.", |
| split: "Split '{{type}}' declarations into multiple statements.", |
| }, |
| }, |
| |
| 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 }; |
| options.using = { uninitialized: mode, initialized: mode }; |
| options.awaitUsing = { 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, |
| }; |
| options.using = { |
| uninitialized: mode.using, |
| initialized: mode.using, |
| }; |
| options.awaitUsing = { |
| uninitialized: mode.awaitUsing, |
| initialized: mode.awaitUsing, |
| }; |
| if (Object.hasOwn(mode, "uninitialized")) { |
| options.var.uninitialized = mode.uninitialized; |
| options.let.uninitialized = mode.uninitialized; |
| options.const.uninitialized = mode.uninitialized; |
| options.using.uninitialized = mode.uninitialized; |
| options.awaitUsing.uninitialized = mode.uninitialized; |
| } |
| if (Object.hasOwn(mode, "initialized")) { |
| options.var.initialized = mode.initialized; |
| options.let.initialized = mode.initialized; |
| options.const.initialized = mode.initialized; |
| options.using.initialized = mode.initialized; |
| options.awaitUsing.initialized = mode.initialized; |
| } |
| } |
| |
| const sourceCode = context.sourceCode; |
| |
| //-------------------------------------------------------------------------- |
| // 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 }, |
| using: { initialized: false, uninitialized: false }, |
| awaitUsing: { 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 one of: "var", "let", "const", "using", or "awaitUsing" |
| * @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 one of: "var", "let", "const", "using", or "awaitUsing" |
| * @returns {Object} The scope associated with statementType |
| */ |
| function getCurrentScope(statementType) { |
| let currentScope; |
| |
| if (statementType === "var") { |
| currentScope = functionStack.at(-1); |
| } else if (statementType === "let") { |
| currentScope = blockStack.at(-1).let; |
| } else if (statementType === "const") { |
| currentScope = blockStack.at(-1).const; |
| } else if (statementType === "using") { |
| currentScope = blockStack.at(-1).using; |
| } else if (statementType === "awaitUsing") { |
| currentScope = blockStack.at(-1).awaitUsing; |
| } |
| 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 one of: "var", "let", "const", "using", or "awaitUsing" |
| * @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 function* joinDeclarationsFixer(fixer) { |
| const type = sourceCode.getFirstToken(declaration.parent); |
| const beforeType = sourceCode.getTokenBefore(type); |
| |
| if ( |
| previousNode && |
| previousNode.kind === declaration.parent.kind |
| ) { |
| if (beforeType.value === ";") { |
| yield fixer.replaceText(beforeType, ","); |
| } else { |
| yield fixer.insertTextAfter(beforeType, ","); |
| } |
| |
| if (declaration.parent.kind === "await using") { |
| const usingToken = sourceCode.getTokenAfter(type); |
| yield fixer.remove(usingToken); |
| } |
| |
| yield fixer.replaceText(type, ""); |
| } |
| }; |
| } |
| |
| /** |
| * Fixer to split a VariableDeclaration into individual declarations |
| * @param {VariableDeclaration} declaration The `VariableDeclaration` to split |
| * @returns {Function|null} The fixer function |
| */ |
| function splitDeclarations(declaration) { |
| const { parent } = declaration; |
| |
| // don't autofix code such as: if (foo) var x, y; |
| if ( |
| !isInStatementList( |
| parent.type === "ExportNamedDeclaration" |
| ? parent |
| : declaration, |
| ) |
| ) { |
| return null; |
| } |
| |
| 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; |
| } |
| |
| const exportPlacement = |
| declaration.parent.type === "ExportNamedDeclaration" |
| ? "export " |
| : ""; |
| |
| /* |
| * `var x,y` |
| * tokenAfterDeclarator ^^ afterComma |
| */ |
| if ( |
| afterComma.range[0] === |
| tokenAfterDeclarator.range[1] |
| ) { |
| return fixer.replaceText( |
| tokenAfterDeclarator, |
| `; ${exportPlacement}${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], |
| )}${exportPlacement}${declaration.kind} `, |
| ); |
| } |
| |
| return fixer.replaceText( |
| tokenAfterDeclarator, |
| `; ${exportPlacement}${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; |
| const key = type === "await using" ? "awaitUsing" : type; |
| |
| if (!options[key]) { |
| return; |
| } |
| |
| const declarations = node.declarations; |
| const declarationCounts = countDeclarations(declarations); |
| const mixedRequires = |
| declarations.some(isRequire) && !declarations.every(isRequire); |
| |
| if (options[key].initialized === MODE_ALWAYS) { |
| if (options.separateRequires && mixedRequires) { |
| context.report({ |
| node, |
| messageId: "splitRequires", |
| }); |
| } |
| } |
| |
| // 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[key].initialized === MODE_CONSECUTIVE && |
| options[key].uninitialized === MODE_CONSECUTIVE |
| ) { |
| context.report({ |
| node, |
| messageId: "combine", |
| data: { |
| type, |
| }, |
| fix: joinDeclarations(declarations), |
| }); |
| } else if ( |
| options[key].initialized === MODE_CONSECUTIVE && |
| declarationCounts.initialized > 0 && |
| previousDeclCounts.initialized > 0 |
| ) { |
| context.report({ |
| node, |
| messageId: "combineInitialized", |
| data: { |
| type, |
| }, |
| fix: joinDeclarations(declarations), |
| }); |
| } else if ( |
| options[key].uninitialized === MODE_CONSECUTIVE && |
| declarationCounts.uninitialized > 0 && |
| previousDeclCounts.uninitialized > 0 |
| ) { |
| context.report({ |
| node, |
| messageId: "combineUninitialized", |
| data: { |
| type, |
| }, |
| fix: joinDeclarations(declarations), |
| }); |
| } |
| } |
| } |
| |
| // always |
| if (!hasOnlyOneStatement(key, declarations)) { |
| if ( |
| options[key].initialized === MODE_ALWAYS && |
| options[key].uninitialized === MODE_ALWAYS |
| ) { |
| context.report({ |
| node, |
| messageId: "combine", |
| data: { |
| type, |
| }, |
| fix: joinDeclarations(declarations), |
| }); |
| } else { |
| if ( |
| options[key].initialized === MODE_ALWAYS && |
| declarationCounts.initialized > 0 |
| ) { |
| context.report({ |
| node, |
| messageId: "combineInitialized", |
| data: { |
| type, |
| }, |
| fix: joinDeclarations(declarations), |
| }); |
| } |
| if ( |
| options[key].uninitialized === MODE_ALWAYS && |
| declarationCounts.uninitialized > 0 |
| ) { |
| if ( |
| node.parent.left === node && |
| (node.parent.type === "ForInStatement" || |
| node.parent.type === "ForOfStatement") |
| ) { |
| return; |
| } |
| context.report({ |
| node, |
| messageId: "combineUninitialized", |
| data: { |
| type, |
| }, |
| fix: joinDeclarations(declarations), |
| }); |
| } |
| } |
| } |
| |
| // never |
| if (parent.type !== "ForStatement" || parent.init !== node) { |
| const totalDeclarations = |
| declarationCounts.uninitialized + |
| declarationCounts.initialized; |
| |
| if (totalDeclarations > 1) { |
| if ( |
| options[key].initialized === MODE_NEVER && |
| options[key].uninitialized === MODE_NEVER |
| ) { |
| // both initialized and uninitialized |
| context.report({ |
| node, |
| messageId: "split", |
| data: { |
| type, |
| }, |
| fix: splitDeclarations(node), |
| }); |
| } else if ( |
| options[key].initialized === MODE_NEVER && |
| declarationCounts.initialized > 0 |
| ) { |
| // initialized |
| context.report({ |
| node, |
| messageId: "splitInitialized", |
| data: { |
| type, |
| }, |
| fix: splitDeclarations(node), |
| }); |
| } else if ( |
| options[key].uninitialized === MODE_NEVER && |
| declarationCounts.uninitialized > 0 |
| ) { |
| // uninitialized |
| context.report({ |
| node, |
| messageId: "splitUninitialized", |
| data: { |
| type, |
| }, |
| fix: splitDeclarations(node), |
| }); |
| } |
| } |
| } |
| } |
| |
| //-------------------------------------------------------------------------- |
| // Public API |
| //-------------------------------------------------------------------------- |
| |
| return { |
| Program: startFunction, |
| FunctionDeclaration: startFunction, |
| FunctionExpression: startFunction, |
| ArrowFunctionExpression: startFunction, |
| StaticBlock: startFunction, // StaticBlock creates a new scope for `var` variables |
| |
| 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, |
| "StaticBlock:exit": endFunction, |
| }; |
| }, |
| }; |