]> git.proxmox.com Git - pve-eslint.git/blame - eslint/lib/rules/one-var.js
import 8.3.0 source
[pve-eslint.git] / eslint / lib / rules / one-var.js
CommitLineData
eb39fafa
DC
1/**
2 * @fileoverview A rule to control the use of single variable declarations.
3 * @author Ian Christian Myers
4 */
5
6"use strict";
7
456be15e
TL
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const astUtils = require("./utils/ast-utils");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18/**
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
22 */
23function isInStatementList(node) {
24 return astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type);
25}
26
eb39fafa
DC
27//------------------------------------------------------------------------------
28// Rule Definition
29//------------------------------------------------------------------------------
30
31module.exports = {
32 meta: {
33 type: "suggestion",
34
35 docs: {
36 description: "enforce variables to be declared either together or separately in functions",
eb39fafa
DC
37 recommended: false,
38 url: "https://eslint.org/docs/rules/one-var"
39 },
40
41 fixable: "code",
42
43 schema: [
44 {
45 oneOf: [
46 {
47 enum: ["always", "never", "consecutive"]
48 },
49 {
50 type: "object",
51 properties: {
52 separateRequires: {
53 type: "boolean"
54 },
55 var: {
56 enum: ["always", "never", "consecutive"]
57 },
58 let: {
59 enum: ["always", "never", "consecutive"]
60 },
61 const: {
62 enum: ["always", "never", "consecutive"]
63 }
64 },
65 additionalProperties: false
66 },
67 {
68 type: "object",
69 properties: {
70 initialized: {
71 enum: ["always", "never", "consecutive"]
72 },
73 uninitialized: {
74 enum: ["always", "never", "consecutive"]
75 }
76 },
77 additionalProperties: false
78 }
79 ]
80 }
81 ],
82
83 messages: {
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."
91 }
92 },
93
94 create(context) {
95 const MODE_ALWAYS = "always";
96 const MODE_NEVER = "never";
97 const MODE_CONSECUTIVE = "consecutive";
98 const mode = context.options[0] || MODE_ALWAYS;
99
100 const options = {};
101
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;
115 }
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;
120 }
121 }
122
123 const sourceCode = context.getSourceCode();
124
125 //--------------------------------------------------------------------------
126 // Helpers
127 //--------------------------------------------------------------------------
128
129 const functionStack = [];
130 const blockStack = [];
131
132 /**
133 * Increments the blockStack counter.
134 * @returns {void}
135 * @private
136 */
137 function startBlock() {
138 blockStack.push({
139 let: { initialized: false, uninitialized: false },
140 const: { initialized: false, uninitialized: false }
141 });
142 }
143
144 /**
145 * Increments the functionStack counter.
146 * @returns {void}
147 * @private
148 */
149 function startFunction() {
150 functionStack.push({ initialized: false, uninitialized: false });
151 startBlock();
152 }
153
154 /**
155 * Decrements the blockStack counter.
156 * @returns {void}
157 * @private
158 */
159 function endBlock() {
160 blockStack.pop();
161 }
162
163 /**
164 * Decrements the functionStack counter.
165 * @returns {void}
166 * @private
167 */
168 function endFunction() {
169 functionStack.pop();
170 endBlock();
171 }
172
173 /**
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.
177 * @private
178 */
179 function isRequire(decl) {
180 return decl.init && decl.init.type === "CallExpression" && decl.init.callee.name === "require";
181 }
182
183 /**
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
188 * @returns {void}
189 * @private
190 */
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;
196 }
197 } else {
198 if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) {
199 if (options.separateRequires && isRequire(declarations[i])) {
200 currentScope.required = true;
201 } else {
202 currentScope.initialized = true;
203 }
204 }
205 }
206 }
207 }
208
209 /**
210 * Determines the current scope (function or block)
609c276f 211 * @param {string} statementType node.kind, one of: "var", "let", or "const"
eb39fafa
DC
212 * @returns {Object} The scope associated with statementType
213 */
214 function getCurrentScope(statementType) {
215 let currentScope;
216
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;
223 }
224 return currentScope;
225 }
226
227 /**
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
231 * @private
232 */
233 function countDeclarations(declarations) {
234 const counts = { uninitialized: 0, initialized: 0 };
235
236 for (let i = 0; i < declarations.length; i++) {
237 if (declarations[i].init === null) {
238 counts.uninitialized++;
239 } else {
240 counts.initialized++;
241 }
242 }
243 return counts;
244 }
245
246 /**
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.
251 * @private
252 */
253 function hasOnlyOneStatement(statementType, declarations) {
254
255 const declarationCounts = countDeclarations(declarations);
256 const currentOptions = options[statementType] || {};
257 const currentScope = getCurrentScope(statementType);
258 const hasRequires = declarations.some(isRequire);
259
260 if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) {
261 if (currentScope.uninitialized || currentScope.initialized) {
262 if (!hasRequires) {
263 return false;
264 }
265 }
266 }
267
268 if (declarationCounts.uninitialized > 0) {
269 if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) {
270 return false;
271 }
272 }
273 if (declarationCounts.initialized > 0) {
274 if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) {
275 if (!hasRequires) {
276 return false;
277 }
278 }
279 }
280 if (currentScope.required && hasRequires) {
281 return false;
282 }
283 recordTypes(statementType, declarations, currentScope);
284 return true;
285 }
286
287 /**
288 * Fixer to join VariableDeclaration's into a single declaration
456be15e
TL
289 * @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join
290 * @returns {Function} The fixer function
eb39fafa
DC
291 */
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];
297
298 return fixer => {
299 const type = sourceCode.getTokenBefore(declaration);
300 const prevSemi = sourceCode.getTokenBefore(type);
301 const res = [];
302
303 if (previousNode && previousNode.kind === sourceCode.getText(type)) {
304 if (prevSemi.value === ";") {
305 res.push(fixer.replaceText(prevSemi, ","));
306 } else {
307 res.push(fixer.insertTextAfter(prevSemi, ","));
308 }
309 res.push(fixer.replaceText(type, ""));
310 }
311
312 return res;
313 };
314 }
315
316 /**
317 * Fixer to split a VariableDeclaration into individual declarations
456be15e
TL
318 * @param {VariableDeclaration} declaration The `VariableDeclaration` to split
319 * @returns {Function|null} The fixer function
eb39fafa
DC
320 */
321 function splitDeclarations(declaration) {
456be15e
TL
322 const { parent } = declaration;
323
324 // don't autofix code such as: if (foo) var x, y;
325 if (!isInStatementList(parent.type === "ExportNamedDeclaration" ? parent : declaration)) {
326 return null;
327 }
328
eb39fafa
DC
329 return fixer => declaration.declarations.map(declarator => {
330 const tokenAfterDeclarator = sourceCode.getTokenAfter(declarator);
331
332 if (tokenAfterDeclarator === null) {
333 return null;
334 }
335
336 const afterComma = sourceCode.getTokenAfter(tokenAfterDeclarator, { includeComments: true });
337
338 if (tokenAfterDeclarator.value !== ",") {
339 return null;
340 }
341
456be15e
TL
342 const exportPlacement = declaration.parent.type === "ExportNamedDeclaration" ? "export " : "";
343
eb39fafa
DC
344 /*
345 * `var x,y`
346 * tokenAfterDeclarator ^^ afterComma
347 */
348 if (afterComma.range[0] === tokenAfterDeclarator.range[1]) {
456be15e 349 return fixer.replaceText(tokenAfterDeclarator, `; ${exportPlacement}${declaration.kind} `);
eb39fafa
DC
350 }
351
352 /*
353 * `var x,
354 * tokenAfterDeclarator ^
355 * y`
356 * ^ afterComma
357 */
358 if (
359 afterComma.loc.start.line > tokenAfterDeclarator.loc.end.line ||
360 afterComma.type === "Line" ||
361 afterComma.type === "Block"
362 ) {
363 let lastComment = afterComma;
364
365 while (lastComment.type === "Line" || lastComment.type === "Block") {
366 lastComment = sourceCode.getTokenAfter(lastComment, { includeComments: true });
367 }
368
369 return fixer.replaceTextRange(
370 [tokenAfterDeclarator.range[0], lastComment.range[0]],
456be15e 371 `;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${exportPlacement}${declaration.kind} `
eb39fafa
DC
372 );
373 }
374
456be15e 375 return fixer.replaceText(tokenAfterDeclarator, `; ${exportPlacement}${declaration.kind}`);
eb39fafa
DC
376 }).filter(x => x);
377 }
378
379 /**
380 * Checks a given VariableDeclaration node for errors.
381 * @param {ASTNode} node The VariableDeclaration node to check
382 * @returns {void}
383 * @private
384 */
385 function checkVariableDeclaration(node) {
386 const parent = node.parent;
387 const type = node.kind;
388
389 if (!options[type]) {
390 return;
391 }
392
393 const declarations = node.declarations;
394 const declarationCounts = countDeclarations(declarations);
395 const mixedRequires = declarations.some(isRequire) && !declarations.every(isRequire);
396
397 if (options[type].initialized === MODE_ALWAYS) {
398 if (options.separateRequires && mixedRequires) {
399 context.report({
400 node,
401 messageId: "splitRequires"
402 });
403 }
404 }
405
406 // consecutive
407 const nodeIndex = (parent.body && parent.body.length > 0 && parent.body.indexOf(node)) || 0;
408
409 if (nodeIndex > 0) {
410 const previousNode = parent.body[nodeIndex - 1];
411 const isPreviousNodeDeclaration = previousNode.type === "VariableDeclaration";
412 const declarationsWithPrevious = declarations.concat(previousNode.declarations || []);
413
414 if (
415 isPreviousNodeDeclaration &&
416 previousNode.kind === type &&
417 !(declarationsWithPrevious.some(isRequire) && !declarationsWithPrevious.every(isRequire))
418 ) {
419 const previousDeclCounts = countDeclarations(previousNode.declarations);
420
421 if (options[type].initialized === MODE_CONSECUTIVE && options[type].uninitialized === MODE_CONSECUTIVE) {
422 context.report({
423 node,
424 messageId: "combine",
425 data: {
426 type
427 },
428 fix: joinDeclarations(declarations)
429 });
430 } else if (options[type].initialized === MODE_CONSECUTIVE && declarationCounts.initialized > 0 && previousDeclCounts.initialized > 0) {
431 context.report({
432 node,
433 messageId: "combineInitialized",
434 data: {
435 type
436 },
437 fix: joinDeclarations(declarations)
438 });
439 } else if (options[type].uninitialized === MODE_CONSECUTIVE &&
440 declarationCounts.uninitialized > 0 &&
441 previousDeclCounts.uninitialized > 0) {
442 context.report({
443 node,
444 messageId: "combineUninitialized",
445 data: {
446 type
447 },
448 fix: joinDeclarations(declarations)
449 });
450 }
451 }
452 }
453
454 // always
455 if (!hasOnlyOneStatement(type, declarations)) {
456 if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) {
457 context.report({
458 node,
459 messageId: "combine",
460 data: {
461 type
462 },
463 fix: joinDeclarations(declarations)
464 });
465 } else {
466 if (options[type].initialized === MODE_ALWAYS && declarationCounts.initialized > 0) {
467 context.report({
468 node,
469 messageId: "combineInitialized",
470 data: {
471 type
472 },
473 fix: joinDeclarations(declarations)
474 });
475 }
476 if (options[type].uninitialized === MODE_ALWAYS && declarationCounts.uninitialized > 0) {
477 if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) {
478 return;
479 }
480 context.report({
481 node,
482 messageId: "combineUninitialized",
483 data: {
484 type
485 },
486 fix: joinDeclarations(declarations)
487 });
488 }
489 }
490 }
491
492 // never
493 if (parent.type !== "ForStatement" || parent.init !== node) {
494 const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized;
495
496 if (totalDeclarations > 1) {
497 if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) {
498
499 // both initialized and uninitialized
500 context.report({
501 node,
502 messageId: "split",
503 data: {
504 type
505 },
506 fix: splitDeclarations(node)
507 });
508 } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) {
509
510 // initialized
511 context.report({
512 node,
513 messageId: "splitInitialized",
514 data: {
515 type
516 },
517 fix: splitDeclarations(node)
518 });
519 } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) {
520
521 // uninitialized
522 context.report({
523 node,
524 messageId: "splitUninitialized",
525 data: {
526 type
527 },
528 fix: splitDeclarations(node)
529 });
530 }
531 }
532 }
533 }
534
535 //--------------------------------------------------------------------------
536 // Public API
537 //--------------------------------------------------------------------------
538
539 return {
540 Program: startFunction,
541 FunctionDeclaration: startFunction,
542 FunctionExpression: startFunction,
543 ArrowFunctionExpression: startFunction,
609c276f
TL
544 StaticBlock: startFunction, // StaticBlock creates a new scope for `var` variables
545
eb39fafa
DC
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,
609c276f 557
eb39fafa
DC
558 "Program:exit": endFunction,
559 "FunctionDeclaration:exit": endFunction,
560 "FunctionExpression:exit": endFunction,
609c276f
TL
561 "ArrowFunctionExpression:exit": endFunction,
562 "StaticBlock:exit": endFunction
eb39fafa
DC
563 };
564
565 }
566};