e79461ce46c5be5705634175415da6030175a860
[dotfiles/.git] / one-var.js
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
12 module.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 };