2 * @fileoverview A rule to control the use of single variable declarations.
3 * @author Ian Christian Myers
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils
= require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
19 * Determines whether the given node is in a statement list.
20 * @param {ASTNode} node node to check
21 * @returns {boolean} `true` if the given node is in a statement list
23 function isInStatementList(node
) {
24 return astUtils
.STATEMENT_LIST_PARENTS
.has(node
.parent
.type
);
27 //------------------------------------------------------------------------------
29 //------------------------------------------------------------------------------
36 description
: "enforce variables to be declared either together or separately in functions",
38 url
: "https://eslint.org/docs/rules/one-var"
47 enum: ["always", "never", "consecutive"]
56 enum: ["always", "never", "consecutive"]
59 enum: ["always", "never", "consecutive"]
62 enum: ["always", "never", "consecutive"]
65 additionalProperties
: false
71 enum: ["always", "never", "consecutive"]
74 enum: ["always", "never", "consecutive"]
77 additionalProperties
: false
84 combineUninitialized
: "Combine this with the previous '{{type}}' statement with uninitialized variables.",
85 combineInitialized
: "Combine this with the previous '{{type}}' statement with initialized variables.",
86 splitUninitialized
: "Split uninitialized '{{type}}' declarations into multiple statements.",
87 splitInitialized
: "Split initialized '{{type}}' declarations into multiple statements.",
88 splitRequires
: "Split requires to be separated into a single block.",
89 combine
: "Combine this with the previous '{{type}}' statement.",
90 split
: "Split '{{type}}' declarations into multiple statements."
95 const MODE_ALWAYS
= "always";
96 const MODE_NEVER
= "never";
97 const MODE_CONSECUTIVE
= "consecutive";
98 const mode
= context
.options
[0] || MODE_ALWAYS
;
102 if (typeof mode
=== "string") { // simple options configuration with just a string
103 options
.var = { uninitialized
: mode
, initialized
: mode
};
104 options
.let = { uninitialized
: mode
, initialized
: mode
};
105 options
.const = { uninitialized
: mode
, initialized
: mode
};
106 } else if (typeof mode
=== "object") { // options configuration is an object
107 options
.separateRequires
= !!mode
.separateRequires
;
108 options
.var = { uninitialized
: mode
.var, initialized
: mode
.var };
109 options
.let = { uninitialized
: mode
.let, initialized
: mode
.let };
110 options
.const = { uninitialized
: mode
.const, initialized
: mode
.const };
111 if (Object
.prototype.hasOwnProperty
.call(mode
, "uninitialized")) {
112 options
.var.uninitialized
= mode
.uninitialized
;
113 options
.let.uninitialized
= mode
.uninitialized
;
114 options
.const.uninitialized
= mode
.uninitialized
;
116 if (Object
.prototype.hasOwnProperty
.call(mode
, "initialized")) {
117 options
.var.initialized
= mode
.initialized
;
118 options
.let.initialized
= mode
.initialized
;
119 options
.const.initialized
= mode
.initialized
;
123 const sourceCode
= context
.getSourceCode();
125 //--------------------------------------------------------------------------
127 //--------------------------------------------------------------------------
129 const functionStack
= [];
130 const blockStack
= [];
133 * Increments the blockStack counter.
137 function startBlock() {
139 let: { initialized
: false, uninitialized
: false },
140 const: { initialized
: false, uninitialized
: false }
145 * Increments the functionStack counter.
149 function startFunction() {
150 functionStack
.push({ initialized
: false, uninitialized
: false });
155 * Decrements the blockStack counter.
159 function endBlock() {
164 * Decrements the functionStack counter.
168 function endFunction() {
174 * Check if a variable declaration is a require.
175 * @param {ASTNode} decl variable declaration Node
176 * @returns {bool} if decl is a require, return true; else return false.
179 function isRequire(decl
) {
180 return decl
.init
&& decl
.init
.type
=== "CallExpression" && decl
.init
.callee
.name
=== "require";
184 * Records whether initialized/uninitialized/required variables are defined in current scope.
185 * @param {string} statementType node.kind, one of: "var", "let", or "const"
186 * @param {ASTNode[]} declarations List of declarations
187 * @param {Object} currentScope The scope being investigated
191 function recordTypes(statementType
, declarations
, currentScope
) {
192 for (let i
= 0; i
< declarations
.length
; i
++) {
193 if (declarations
[i
].init
=== null) {
194 if (options
[statementType
] && options
[statementType
].uninitialized
=== MODE_ALWAYS
) {
195 currentScope
.uninitialized
= true;
198 if (options
[statementType
] && options
[statementType
].initialized
=== MODE_ALWAYS
) {
199 if (options
.separateRequires
&& isRequire(declarations
[i
])) {
200 currentScope
.required
= true;
202 currentScope
.initialized
= true;
210 * Determines the current scope (function or block)
211 * @param {string} statementType node.kind, one of: "var", "let", or "const"
212 * @returns {Object} The scope associated with statementType
214 function getCurrentScope(statementType
) {
217 if (statementType
=== "var") {
218 currentScope
= functionStack
[functionStack
.length
- 1];
219 } else if (statementType
=== "let") {
220 currentScope
= blockStack
[blockStack
.length
- 1].let;
221 } else if (statementType
=== "const") {
222 currentScope
= blockStack
[blockStack
.length
- 1].const;
228 * Counts the number of initialized and uninitialized declarations in a list of declarations
229 * @param {ASTNode[]} declarations List of declarations
230 * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations
233 function countDeclarations(declarations
) {
234 const counts
= { uninitialized
: 0, initialized
: 0 };
236 for (let i
= 0; i
< declarations
.length
; i
++) {
237 if (declarations
[i
].init
=== null) {
238 counts
.uninitialized
++;
240 counts
.initialized
++;
247 * Determines if there is more than one var statement in the current scope.
248 * @param {string} statementType node.kind, one of: "var", "let", or "const"
249 * @param {ASTNode[]} declarations List of declarations
250 * @returns {boolean} Returns true if it is the first var declaration, false if not.
253 function hasOnlyOneStatement(statementType
, declarations
) {
255 const declarationCounts
= countDeclarations(declarations
);
256 const currentOptions
= options
[statementType
] || {};
257 const currentScope
= getCurrentScope(statementType
);
258 const hasRequires
= declarations
.some(isRequire
);
260 if (currentOptions
.uninitialized
=== MODE_ALWAYS
&& currentOptions
.initialized
=== MODE_ALWAYS
) {
261 if (currentScope
.uninitialized
|| currentScope
.initialized
) {
268 if (declarationCounts
.uninitialized
> 0) {
269 if (currentOptions
.uninitialized
=== MODE_ALWAYS
&& currentScope
.uninitialized
) {
273 if (declarationCounts
.initialized
> 0) {
274 if (currentOptions
.initialized
=== MODE_ALWAYS
&& currentScope
.initialized
) {
280 if (currentScope
.required
&& hasRequires
) {
283 recordTypes(statementType
, declarations
, currentScope
);
288 * Fixer to join VariableDeclaration's into a single declaration
289 * @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join
290 * @returns {Function} The fixer function
292 function joinDeclarations(declarations
) {
293 const declaration
= declarations
[0];
294 const body
= Array
.isArray(declaration
.parent
.parent
.body
) ? declaration
.parent
.parent
.body
: [];
295 const currentIndex
= body
.findIndex(node
=> node
.range
[0] === declaration
.parent
.range
[0]);
296 const previousNode
= body
[currentIndex
- 1];
299 const type
= sourceCode
.getTokenBefore(declaration
);
300 const prevSemi
= sourceCode
.getTokenBefore(type
);
303 if (previousNode
&& previousNode
.kind
=== sourceCode
.getText(type
)) {
304 if (prevSemi
.value
=== ";") {
305 res
.push(fixer
.replaceText(prevSemi
, ","));
307 res
.push(fixer
.insertTextAfter(prevSemi
, ","));
309 res
.push(fixer
.replaceText(type
, ""));
317 * Fixer to split a VariableDeclaration into individual declarations
318 * @param {VariableDeclaration} declaration The `VariableDeclaration` to split
319 * @returns {Function|null} The fixer function
321 function splitDeclarations(declaration
) {
322 const { parent
} = declaration
;
324 // don't autofix code such as: if (foo) var x, y;
325 if (!isInStatementList(parent
.type
=== "ExportNamedDeclaration" ? parent
: declaration
)) {
329 return fixer
=> declaration
.declarations
.map(declarator
=> {
330 const tokenAfterDeclarator
= sourceCode
.getTokenAfter(declarator
);
332 if (tokenAfterDeclarator
=== null) {
336 const afterComma
= sourceCode
.getTokenAfter(tokenAfterDeclarator
, { includeComments
: true });
338 if (tokenAfterDeclarator
.value
!== ",") {
342 const exportPlacement
= declaration
.parent
.type
=== "ExportNamedDeclaration" ? "export " : "";
346 * tokenAfterDeclarator ^^ afterComma
348 if (afterComma
.range
[0] === tokenAfterDeclarator
.range
[1]) {
349 return fixer
.replaceText(tokenAfterDeclarator
, `; ${exportPlacement}${declaration.kind} `);
354 * tokenAfterDeclarator ^
359 afterComma
.loc
.start
.line
> tokenAfterDeclarator
.loc
.end
.line
||
360 afterComma
.type
=== "Line" ||
361 afterComma
.type
=== "Block"
363 let lastComment
= afterComma
;
365 while (lastComment
.type
=== "Line" || lastComment
.type
=== "Block") {
366 lastComment
= sourceCode
.getTokenAfter(lastComment
, { includeComments
: true });
369 return fixer
.replaceTextRange(
370 [tokenAfterDeclarator
.range
[0], lastComment
.range
[0]],
371 `;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${exportPlacement}${declaration.kind} `
375 return fixer
.replaceText(tokenAfterDeclarator
, `; ${exportPlacement}${declaration.kind}`);
380 * Checks a given VariableDeclaration node for errors.
381 * @param {ASTNode} node The VariableDeclaration node to check
385 function checkVariableDeclaration(node
) {
386 const parent
= node
.parent
;
387 const type
= node
.kind
;
389 if (!options
[type
]) {
393 const declarations
= node
.declarations
;
394 const declarationCounts
= countDeclarations(declarations
);
395 const mixedRequires
= declarations
.some(isRequire
) && !declarations
.every(isRequire
);
397 if (options
[type
].initialized
=== MODE_ALWAYS
) {
398 if (options
.separateRequires
&& mixedRequires
) {
401 messageId
: "splitRequires"
407 const nodeIndex
= (parent
.body
&& parent
.body
.length
> 0 && parent
.body
.indexOf(node
)) || 0;
410 const previousNode
= parent
.body
[nodeIndex
- 1];
411 const isPreviousNodeDeclaration
= previousNode
.type
=== "VariableDeclaration";
412 const declarationsWithPrevious
= declarations
.concat(previousNode
.declarations
|| []);
415 isPreviousNodeDeclaration
&&
416 previousNode
.kind
=== type
&&
417 !(declarationsWithPrevious
.some(isRequire
) && !declarationsWithPrevious
.every(isRequire
))
419 const previousDeclCounts
= countDeclarations(previousNode
.declarations
);
421 if (options
[type
].initialized
=== MODE_CONSECUTIVE
&& options
[type
].uninitialized
=== MODE_CONSECUTIVE
) {
424 messageId
: "combine",
428 fix
: joinDeclarations(declarations
)
430 } else if (options
[type
].initialized
=== MODE_CONSECUTIVE
&& declarationCounts
.initialized
> 0 && previousDeclCounts
.initialized
> 0) {
433 messageId
: "combineInitialized",
437 fix
: joinDeclarations(declarations
)
439 } else if (options
[type
].uninitialized
=== MODE_CONSECUTIVE
&&
440 declarationCounts
.uninitialized
> 0 &&
441 previousDeclCounts
.uninitialized
> 0) {
444 messageId
: "combineUninitialized",
448 fix
: joinDeclarations(declarations
)
455 if (!hasOnlyOneStatement(type
, declarations
)) {
456 if (options
[type
].initialized
=== MODE_ALWAYS
&& options
[type
].uninitialized
=== MODE_ALWAYS
) {
459 messageId
: "combine",
463 fix
: joinDeclarations(declarations
)
466 if (options
[type
].initialized
=== MODE_ALWAYS
&& declarationCounts
.initialized
> 0) {
469 messageId
: "combineInitialized",
473 fix
: joinDeclarations(declarations
)
476 if (options
[type
].uninitialized
=== MODE_ALWAYS
&& declarationCounts
.uninitialized
> 0) {
477 if (node
.parent
.left
=== node
&& (node
.parent
.type
=== "ForInStatement" || node
.parent
.type
=== "ForOfStatement")) {
482 messageId
: "combineUninitialized",
486 fix
: joinDeclarations(declarations
)
493 if (parent
.type
!== "ForStatement" || parent
.init
!== node
) {
494 const totalDeclarations
= declarationCounts
.uninitialized
+ declarationCounts
.initialized
;
496 if (totalDeclarations
> 1) {
497 if (options
[type
].initialized
=== MODE_NEVER
&& options
[type
].uninitialized
=== MODE_NEVER
) {
499 // both initialized and uninitialized
506 fix
: splitDeclarations(node
)
508 } else if (options
[type
].initialized
=== MODE_NEVER
&& declarationCounts
.initialized
> 0) {
513 messageId
: "splitInitialized",
517 fix
: splitDeclarations(node
)
519 } else if (options
[type
].uninitialized
=== MODE_NEVER
&& declarationCounts
.uninitialized
> 0) {
524 messageId
: "splitUninitialized",
528 fix
: splitDeclarations(node
)
535 //--------------------------------------------------------------------------
537 //--------------------------------------------------------------------------
540 Program
: startFunction
,
541 FunctionDeclaration
: startFunction
,
542 FunctionExpression
: startFunction
,
543 ArrowFunctionExpression
: startFunction
,
544 StaticBlock
: startFunction
, // StaticBlock creates a new scope for `var` variables
546 BlockStatement
: startBlock
,
547 ForStatement
: startBlock
,
548 ForInStatement
: startBlock
,
549 ForOfStatement
: startBlock
,
550 SwitchStatement
: startBlock
,
551 VariableDeclaration
: checkVariableDeclaration
,
552 "ForStatement:exit": endBlock
,
553 "ForOfStatement:exit": endBlock
,
554 "ForInStatement:exit": endBlock
,
555 "SwitchStatement:exit": endBlock
,
556 "BlockStatement:exit": endBlock
,
558 "Program:exit": endFunction
,
559 "FunctionDeclaration:exit": endFunction
,
560 "FunctionExpression:exit": endFunction
,
561 "ArrowFunctionExpression:exit": endFunction
,
562 "StaticBlock:exit": endFunction