UNPKG

21.5 kBJavaScriptView Raw
1/**
2 * @fileoverview A rule to control the use of single variable declarations.
3 * @author Ian Christian Myers
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Rule Definition
10//------------------------------------------------------------------------------
11
12module.exports = {
13 meta: {
14 type: "suggestion",
15
16 docs: {
17 description: "enforce variables to be declared either together or separately in functions",
18 category: "Stylistic Issues",
19 recommended: false,
20 url: "https://eslint.org/docs/rules/one-var"
21 },
22
23 fixable: "code",
24
25 schema: [
26 {
27 oneOf: [
28 {
29 enum: ["always", "never", "consecutive"]
30 },
31 {
32 type: "object",
33 properties: {
34 separateRequires: {
35 type: "boolean"
36 },
37 var: {
38 enum: ["always", "never", "consecutive"]
39 },
40 let: {
41 enum: ["always", "never", "consecutive"]
42 },
43 const: {
44 enum: ["always", "never", "consecutive"]
45 }
46 },
47 additionalProperties: false
48 },
49 {
50 type: "object",
51 properties: {
52 initialized: {
53 enum: ["always", "never", "consecutive"]
54 },
55 uninitialized: {
56 enum: ["always", "never", "consecutive"]
57 }
58 },
59 additionalProperties: false
60 }
61 ]
62 }
63 ]
64 },
65
66 create(context) {
67 const MODE_ALWAYS = "always";
68 const MODE_NEVER = "never";
69 const MODE_CONSECUTIVE = "consecutive";
70 const mode = context.options[0] || MODE_ALWAYS;
71
72 const options = {};
73
74 if (typeof mode === "string") { // simple options configuration with just a string
75 options.var = { uninitialized: mode, initialized: mode };
76 options.let = { uninitialized: mode, initialized: mode };
77 options.const = { uninitialized: mode, initialized: mode };
78 } else if (typeof mode === "object") { // options configuration is an object
79 options.separateRequires = !!mode.separateRequires;
80 options.var = { uninitialized: mode.var, initialized: mode.var };
81 options.let = { uninitialized: mode.let, initialized: mode.let };
82 options.const = { uninitialized: mode.const, initialized: mode.const };
83 if (Object.prototype.hasOwnProperty.call(mode, "uninitialized")) {
84 options.var.uninitialized = mode.uninitialized;
85 options.let.uninitialized = mode.uninitialized;
86 options.const.uninitialized = mode.uninitialized;
87 }
88 if (Object.prototype.hasOwnProperty.call(mode, "initialized")) {
89 options.var.initialized = mode.initialized;
90 options.let.initialized = mode.initialized;
91 options.const.initialized = mode.initialized;
92 }
93 }
94
95 const sourceCode = context.getSourceCode();
96
97 //--------------------------------------------------------------------------
98 // Helpers
99 //--------------------------------------------------------------------------
100
101 const functionStack = [];
102 const blockStack = [];
103
104 /**
105 * Increments the blockStack counter.
106 * @returns {void}
107 * @private
108 */
109 function startBlock() {
110 blockStack.push({
111 let: { initialized: false, uninitialized: false },
112 const: { initialized: false, uninitialized: false }
113 });
114 }
115
116 /**
117 * Increments the functionStack counter.
118 * @returns {void}
119 * @private
120 */
121 function startFunction() {
122 functionStack.push({ initialized: false, uninitialized: false });
123 startBlock();
124 }
125
126 /**
127 * Decrements the blockStack counter.
128 * @returns {void}
129 * @private
130 */
131 function endBlock() {
132 blockStack.pop();
133 }
134
135 /**
136 * Decrements the functionStack counter.
137 * @returns {void}
138 * @private
139 */
140 function endFunction() {
141 functionStack.pop();
142 endBlock();
143 }
144
145 /**
146 * Check if a variable declaration is a require.
147 * @param {ASTNode} decl variable declaration Node
148 * @returns {bool} if decl is a require, return true; else return false.
149 * @private
150 */
151 function isRequire(decl) {
152 return decl.init && decl.init.type === "CallExpression" && decl.init.callee.name === "require";
153 }
154
155 /**
156 * Records whether initialized/uninitialized/required variables are defined in current scope.
157 * @param {string} statementType node.kind, one of: "var", "let", or "const"
158 * @param {ASTNode[]} declarations List of declarations
159 * @param {Object} currentScope The scope being investigated
160 * @returns {void}
161 * @private
162 */
163 function recordTypes(statementType, declarations, currentScope) {
164 for (let i = 0; i < declarations.length; i++) {
165 if (declarations[i].init === null) {
166 if (options[statementType] && options[statementType].uninitialized === MODE_ALWAYS) {
167 currentScope.uninitialized = true;
168 }
169 } else {
170 if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) {
171 if (options.separateRequires && isRequire(declarations[i])) {
172 currentScope.required = true;
173 } else {
174 currentScope.initialized = true;
175 }
176 }
177 }
178 }
179 }
180
181 /**
182 * Determines the current scope (function or block)
183 * @param {string} statementType node.kind, one of: "var", "let", or "const"
184 * @returns {Object} The scope associated with statementType
185 */
186 function getCurrentScope(statementType) {
187 let currentScope;
188
189 if (statementType === "var") {
190 currentScope = functionStack[functionStack.length - 1];
191 } else if (statementType === "let") {
192 currentScope = blockStack[blockStack.length - 1].let;
193 } else if (statementType === "const") {
194 currentScope = blockStack[blockStack.length - 1].const;
195 }
196 return currentScope;
197 }
198
199 /**
200 * Counts the number of initialized and uninitialized declarations in a list of declarations
201 * @param {ASTNode[]} declarations List of declarations
202 * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations
203 * @private
204 */
205 function countDeclarations(declarations) {
206 const counts = { uninitialized: 0, initialized: 0 };
207
208 for (let i = 0; i < declarations.length; i++) {
209 if (declarations[i].init === null) {
210 counts.uninitialized++;
211 } else {
212 counts.initialized++;
213 }
214 }
215 return counts;
216 }
217
218 /**
219 * Determines if there is more than one var statement in the current scope.
220 * @param {string} statementType node.kind, one of: "var", "let", or "const"
221 * @param {ASTNode[]} declarations List of declarations
222 * @returns {boolean} Returns true if it is the first var declaration, false if not.
223 * @private
224 */
225 function hasOnlyOneStatement(statementType, declarations) {
226
227 const declarationCounts = countDeclarations(declarations);
228 const currentOptions = options[statementType] || {};
229 const currentScope = getCurrentScope(statementType);
230 const hasRequires = declarations.some(isRequire);
231
232 if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) {
233 if (currentScope.uninitialized || currentScope.initialized) {
234 if (!hasRequires) {
235 return false;
236 }
237 }
238 }
239
240 if (declarationCounts.uninitialized > 0) {
241 if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) {
242 return false;
243 }
244 }
245 if (declarationCounts.initialized > 0) {
246 if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) {
247 if (!hasRequires) {
248 return false;
249 }
250 }
251 }
252 if (currentScope.required && hasRequires) {
253 return false;
254 }
255 recordTypes(statementType, declarations, currentScope);
256 return true;
257 }
258
259 /**
260 * Fixer to join VariableDeclaration's into a single declaration
261 * @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join
262 * @returns {Function} The fixer function
263 */
264 function joinDeclarations(declarations) {
265 const declaration = declarations[0];
266 const body = Array.isArray(declaration.parent.parent.body) ? declaration.parent.parent.body : [];
267 const currentIndex = body.findIndex(node => node.range[0] === declaration.parent.range[0]);
268 const previousNode = body[currentIndex - 1];
269
270 return fixer => {
271 const type = sourceCode.getTokenBefore(declaration);
272 const prevSemi = sourceCode.getTokenBefore(type);
273 const res = [];
274
275 if (previousNode && previousNode.kind === sourceCode.getText(type)) {
276 if (prevSemi.value === ";") {
277 res.push(fixer.replaceText(prevSemi, ","));
278 } else {
279 res.push(fixer.insertTextAfter(prevSemi, ","));
280 }
281 res.push(fixer.replaceText(type, ""));
282 }
283
284 return res;
285 };
286 }
287
288 /**
289 * Fixer to split a VariableDeclaration into individual declarations
290 * @param {VariableDeclaration} declaration The `VariableDeclaration` to split
291 * @returns {Function} The fixer function
292 */
293 function splitDeclarations(declaration) {
294 return fixer => declaration.declarations.map(declarator => {
295 const tokenAfterDeclarator = sourceCode.getTokenAfter(declarator);
296
297 if (tokenAfterDeclarator === null) {
298 return null;
299 }
300
301 const afterComma = sourceCode.getTokenAfter(tokenAfterDeclarator, { includeComments: true });
302
303 if (tokenAfterDeclarator.value !== ",") {
304 return null;
305 }
306
307 /*
308 * `var x,y`
309 * tokenAfterDeclarator ^^ afterComma
310 */
311 if (afterComma.range[0] === tokenAfterDeclarator.range[1]) {
312 return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind} `);
313 }
314
315 /*
316 * `var x,
317 * tokenAfterDeclarator ^
318 * y`
319 * ^ afterComma
320 */
321 if (
322 afterComma.loc.start.line > tokenAfterDeclarator.loc.end.line ||
323 afterComma.type === "Line" ||
324 afterComma.type === "Block"
325 ) {
326 let lastComment = afterComma;
327
328 while (lastComment.type === "Line" || lastComment.type === "Block") {
329 lastComment = sourceCode.getTokenAfter(lastComment, { includeComments: true });
330 }
331
332 return fixer.replaceTextRange(
333 [tokenAfterDeclarator.range[0], lastComment.range[0]],
334 `;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${declaration.kind} `
335 );
336 }
337
338 return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind}`);
339 }).filter(x => x);
340 }
341
342 /**
343 * Checks a given VariableDeclaration node for errors.
344 * @param {ASTNode} node The VariableDeclaration node to check
345 * @returns {void}
346 * @private
347 */
348 function checkVariableDeclaration(node) {
349 const parent = node.parent;
350 const type = node.kind;
351
352 if (!options[type]) {
353 return;
354 }
355
356 const declarations = node.declarations;
357 const declarationCounts = countDeclarations(declarations);
358 const mixedRequires = declarations.some(isRequire) && !declarations.every(isRequire);
359
360 if (options[type].initialized === MODE_ALWAYS) {
361 if (options.separateRequires && mixedRequires) {
362 context.report({
363 node,
364 message: "Split requires to be separated into a single block."
365 });
366 }
367 }
368
369 // consecutive
370 const nodeIndex = (parent.body && parent.body.length > 0 && parent.body.indexOf(node)) || 0;
371
372 if (nodeIndex > 0) {
373 const previousNode = parent.body[nodeIndex - 1];
374 const isPreviousNodeDeclaration = previousNode.type === "VariableDeclaration";
375 const declarationsWithPrevious = declarations.concat(previousNode.declarations || []);
376
377 if (
378 isPreviousNodeDeclaration &&
379 previousNode.kind === type &&
380 !(declarationsWithPrevious.some(isRequire) && !declarationsWithPrevious.every(isRequire))
381 ) {
382 const previousDeclCounts = countDeclarations(previousNode.declarations);
383
384 if (options[type].initialized === MODE_CONSECUTIVE && options[type].uninitialized === MODE_CONSECUTIVE) {
385 context.report({
386 node,
387 message: "Combine this with the previous '{{type}}' statement.",
388 data: {
389 type
390 },
391 fix: joinDeclarations(declarations)
392 });
393 } else if (options[type].initialized === MODE_CONSECUTIVE && declarationCounts.initialized > 0 && previousDeclCounts.initialized > 0) {
394 context.report({
395 node,
396 message: "Combine this with the previous '{{type}}' statement with initialized variables.",
397 data: {
398 type
399 },
400 fix: joinDeclarations(declarations)
401 });
402 } else if (options[type].uninitialized === MODE_CONSECUTIVE &&
403 declarationCounts.uninitialized > 0 &&
404 previousDeclCounts.uninitialized > 0) {
405 context.report({
406 node,
407 message: "Combine this with the previous '{{type}}' statement with uninitialized variables.",
408 data: {
409 type
410 },
411 fix: joinDeclarations(declarations)
412 });
413 }
414 }
415 }
416
417 // always
418 if (!hasOnlyOneStatement(type, declarations)) {
419 if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) {
420 context.report({
421 node,
422 message: "Combine this with the previous '{{type}}' statement.",
423 data: {
424 type
425 },
426 fix: joinDeclarations(declarations)
427 });
428 } else {
429 if (options[type].initialized === MODE_ALWAYS && declarationCounts.initialized > 0) {
430 context.report({
431 node,
432 message: "Combine this with the previous '{{type}}' statement with initialized variables.",
433 data: {
434 type
435 },
436 fix: joinDeclarations(declarations)
437 });
438 }
439 if (options[type].uninitialized === MODE_ALWAYS && declarationCounts.uninitialized > 0) {
440 if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) {
441 return;
442 }
443 context.report({
444 node,
445 message: "Combine this with the previous '{{type}}' statement with uninitialized variables.",
446 data: {
447 type
448 },
449 fix: joinDeclarations(declarations)
450 });
451 }
452 }
453 }
454
455 // never
456 if (parent.type !== "ForStatement" || parent.init !== node) {
457 const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized;
458
459 if (totalDeclarations > 1) {
460 if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) {
461
462 // both initialized and uninitialized
463 context.report({
464 node,
465 message: "Split '{{type}}' declarations into multiple statements.",
466 data: {
467 type
468 },
469 fix: splitDeclarations(node)
470 });
471 } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) {
472
473 // initialized
474 context.report({
475 node,
476 message: "Split initialized '{{type}}' declarations into multiple statements.",
477 data: {
478 type
479 },
480 fix: splitDeclarations(node)
481 });
482 } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) {
483
484 // uninitialized
485 context.report({
486 node,
487 message: "Split uninitialized '{{type}}' declarations into multiple statements.",
488 data: {
489 type
490 },
491 fix: splitDeclarations(node)
492 });
493 }
494 }
495 }
496 }
497
498 //--------------------------------------------------------------------------
499 // Public API
500 //--------------------------------------------------------------------------
501
502 return {
503 Program: startFunction,
504 FunctionDeclaration: startFunction,
505 FunctionExpression: startFunction,
506 ArrowFunctionExpression: startFunction,
507 BlockStatement: startBlock,
508 ForStatement: startBlock,
509 ForInStatement: startBlock,
510 ForOfStatement: startBlock,
511 SwitchStatement: startBlock,
512 VariableDeclaration: checkVariableDeclaration,
513 "ForStatement:exit": endBlock,
514 "ForOfStatement:exit": endBlock,
515 "ForInStatement:exit": endBlock,
516 "SwitchStatement:exit": endBlock,
517 "BlockStatement:exit": endBlock,
518 "Program:exit": endFunction,
519 "FunctionDeclaration:exit": endFunction,
520 "FunctionExpression:exit": endFunction,
521 "ArrowFunctionExpression:exit": endFunction
522 };
523
524 }
525};