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