• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Rule to require or disallow newlines between statements
3 * @author Toru Nagashima
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const astUtils = require("./utils/ast-utils");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18const LT = `[${Array.from(astUtils.LINEBREAKS).join("")}]`;
19const PADDING_LINE_SEQUENCE = new RegExp(
20    String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`,
21    "u"
22);
23const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u;
24const CJS_IMPORT = /^require\(/u;
25
26/**
27 * Creates tester which check if a node starts with specific keyword.
28 * @param {string} keyword The keyword to test.
29 * @returns {Object} the created tester.
30 * @private
31 */
32function newKeywordTester(keyword) {
33    return {
34        test: (node, sourceCode) =>
35            sourceCode.getFirstToken(node).value === keyword
36    };
37}
38
39/**
40 * Creates tester which check if a node starts with specific keyword and spans a single line.
41 * @param {string} keyword The keyword to test.
42 * @returns {Object} the created tester.
43 * @private
44 */
45function newSinglelineKeywordTester(keyword) {
46    return {
47        test: (node, sourceCode) =>
48            node.loc.start.line === node.loc.end.line &&
49            sourceCode.getFirstToken(node).value === keyword
50    };
51}
52
53/**
54 * Creates tester which check if a node starts with specific keyword and spans multiple lines.
55 * @param {string} keyword The keyword to test.
56 * @returns {Object} the created tester.
57 * @private
58 */
59function newMultilineKeywordTester(keyword) {
60    return {
61        test: (node, sourceCode) =>
62            node.loc.start.line !== node.loc.end.line &&
63            sourceCode.getFirstToken(node).value === keyword
64    };
65}
66
67/**
68 * Creates tester which check if a node is specific type.
69 * @param {string} type The node type to test.
70 * @returns {Object} the created tester.
71 * @private
72 */
73function newNodeTypeTester(type) {
74    return {
75        test: node =>
76            node.type === type
77    };
78}
79
80/**
81 * Checks the given node is an expression statement of IIFE.
82 * @param {ASTNode} node The node to check.
83 * @returns {boolean} `true` if the node is an expression statement of IIFE.
84 * @private
85 */
86function isIIFEStatement(node) {
87    if (node.type === "ExpressionStatement") {
88        let call = astUtils.skipChainExpression(node.expression);
89
90        if (call.type === "UnaryExpression") {
91            call = astUtils.skipChainExpression(call.argument);
92        }
93        return call.type === "CallExpression" && astUtils.isFunction(call.callee);
94    }
95    return false;
96}
97
98/**
99 * Checks whether the given node is a block-like statement.
100 * This checks the last token of the node is the closing brace of a block.
101 * @param {SourceCode} sourceCode The source code to get tokens.
102 * @param {ASTNode} node The node to check.
103 * @returns {boolean} `true` if the node is a block-like statement.
104 * @private
105 */
106function isBlockLikeStatement(sourceCode, node) {
107
108    // do-while with a block is a block-like statement.
109    if (node.type === "DoWhileStatement" && node.body.type === "BlockStatement") {
110        return true;
111    }
112
113    /*
114     * IIFE is a block-like statement specially from
115     * JSCS#disallowPaddingNewLinesAfterBlocks.
116     */
117    if (isIIFEStatement(node)) {
118        return true;
119    }
120
121    // Checks the last token is a closing brace of blocks.
122    const lastToken = sourceCode.getLastToken(node, astUtils.isNotSemicolonToken);
123    const belongingNode = lastToken && astUtils.isClosingBraceToken(lastToken)
124        ? sourceCode.getNodeByRangeIndex(lastToken.range[0])
125        : null;
126
127    return Boolean(belongingNode) && (
128        belongingNode.type === "BlockStatement" ||
129        belongingNode.type === "SwitchStatement"
130    );
131}
132
133/**
134 * Check whether the given node is a directive or not.
135 * @param {ASTNode} node The node to check.
136 * @param {SourceCode} sourceCode The source code object to get tokens.
137 * @returns {boolean} `true` if the node is a directive.
138 */
139function isDirective(node, sourceCode) {
140    return (
141        node.type === "ExpressionStatement" &&
142        (
143            node.parent.type === "Program" ||
144            (
145                node.parent.type === "BlockStatement" &&
146                astUtils.isFunction(node.parent.parent)
147            )
148        ) &&
149        node.expression.type === "Literal" &&
150        typeof node.expression.value === "string" &&
151        !astUtils.isParenthesised(sourceCode, node.expression)
152    );
153}
154
155/**
156 * Check whether the given node is a part of directive prologue or not.
157 * @param {ASTNode} node The node to check.
158 * @param {SourceCode} sourceCode The source code object to get tokens.
159 * @returns {boolean} `true` if the node is a part of directive prologue.
160 */
161function isDirectivePrologue(node, sourceCode) {
162    if (isDirective(node, sourceCode)) {
163        for (const sibling of node.parent.body) {
164            if (sibling === node) {
165                break;
166            }
167            if (!isDirective(sibling, sourceCode)) {
168                return false;
169            }
170        }
171        return true;
172    }
173    return false;
174}
175
176/**
177 * Gets the actual last token.
178 *
179 * If a semicolon is semicolon-less style's semicolon, this ignores it.
180 * For example:
181 *
182 *     foo()
183 *     ;[1, 2, 3].forEach(bar)
184 * @param {SourceCode} sourceCode The source code to get tokens.
185 * @param {ASTNode} node The node to get.
186 * @returns {Token} The actual last token.
187 * @private
188 */
189function getActualLastToken(sourceCode, node) {
190    const semiToken = sourceCode.getLastToken(node);
191    const prevToken = sourceCode.getTokenBefore(semiToken);
192    const nextToken = sourceCode.getTokenAfter(semiToken);
193    const isSemicolonLessStyle = Boolean(
194        prevToken &&
195        nextToken &&
196        prevToken.range[0] >= node.range[0] &&
197        astUtils.isSemicolonToken(semiToken) &&
198        semiToken.loc.start.line !== prevToken.loc.end.line &&
199        semiToken.loc.end.line === nextToken.loc.start.line
200    );
201
202    return isSemicolonLessStyle ? prevToken : semiToken;
203}
204
205/**
206 * This returns the concatenation of the first 2 captured strings.
207 * @param {string} _ Unused. Whole matched string.
208 * @param {string} trailingSpaces The trailing spaces of the first line.
209 * @param {string} indentSpaces The indentation spaces of the last line.
210 * @returns {string} The concatenation of trailingSpaces and indentSpaces.
211 * @private
212 */
213function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) {
214    return trailingSpaces + indentSpaces;
215}
216
217/**
218 * Check and report statements for `any` configuration.
219 * It does nothing.
220 * @returns {void}
221 * @private
222 */
223function verifyForAny() {
224}
225
226/**
227 * Check and report statements for `never` configuration.
228 * This autofix removes blank lines between the given 2 statements.
229 * However, if comments exist between 2 blank lines, it does not remove those
230 * blank lines automatically.
231 * @param {RuleContext} context The rule context to report.
232 * @param {ASTNode} _ Unused. The previous node to check.
233 * @param {ASTNode} nextNode The next node to check.
234 * @param {Array<Token[]>} paddingLines The array of token pairs that blank
235 * lines exist between the pair.
236 * @returns {void}
237 * @private
238 */
239function verifyForNever(context, _, nextNode, paddingLines) {
240    if (paddingLines.length === 0) {
241        return;
242    }
243
244    context.report({
245        node: nextNode,
246        messageId: "unexpectedBlankLine",
247        fix(fixer) {
248            if (paddingLines.length >= 2) {
249                return null;
250            }
251
252            const prevToken = paddingLines[0][0];
253            const nextToken = paddingLines[0][1];
254            const start = prevToken.range[1];
255            const end = nextToken.range[0];
256            const text = context.getSourceCode().text
257                .slice(start, end)
258                .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines);
259
260            return fixer.replaceTextRange([start, end], text);
261        }
262    });
263}
264
265/**
266 * Check and report statements for `always` configuration.
267 * This autofix inserts a blank line between the given 2 statements.
268 * If the `prevNode` has trailing comments, it inserts a blank line after the
269 * trailing comments.
270 * @param {RuleContext} context The rule context to report.
271 * @param {ASTNode} prevNode The previous node to check.
272 * @param {ASTNode} nextNode The next node to check.
273 * @param {Array<Token[]>} paddingLines The array of token pairs that blank
274 * lines exist between the pair.
275 * @returns {void}
276 * @private
277 */
278function verifyForAlways(context, prevNode, nextNode, paddingLines) {
279    if (paddingLines.length > 0) {
280        return;
281    }
282
283    context.report({
284        node: nextNode,
285        messageId: "expectedBlankLine",
286        fix(fixer) {
287            const sourceCode = context.getSourceCode();
288            let prevToken = getActualLastToken(sourceCode, prevNode);
289            const nextToken = sourceCode.getFirstTokenBetween(
290                prevToken,
291                nextNode,
292                {
293                    includeComments: true,
294
295                    /**
296                     * Skip the trailing comments of the previous node.
297                     * This inserts a blank line after the last trailing comment.
298                     *
299                     * For example:
300                     *
301                     *     foo(); // trailing comment.
302                     *     // comment.
303                     *     bar();
304                     *
305                     * Get fixed to:
306                     *
307                     *     foo(); // trailing comment.
308                     *
309                     *     // comment.
310                     *     bar();
311                     * @param {Token} token The token to check.
312                     * @returns {boolean} `true` if the token is not a trailing comment.
313                     * @private
314                     */
315                    filter(token) {
316                        if (astUtils.isTokenOnSameLine(prevToken, token)) {
317                            prevToken = token;
318                            return false;
319                        }
320                        return true;
321                    }
322                }
323            ) || nextNode;
324            const insertText = astUtils.isTokenOnSameLine(prevToken, nextToken)
325                ? "\n\n"
326                : "\n";
327
328            return fixer.insertTextAfter(prevToken, insertText);
329        }
330    });
331}
332
333/**
334 * Types of blank lines.
335 * `any`, `never`, and `always` are defined.
336 * Those have `verify` method to check and report statements.
337 * @private
338 */
339const PaddingTypes = {
340    any: { verify: verifyForAny },
341    never: { verify: verifyForNever },
342    always: { verify: verifyForAlways }
343};
344
345/**
346 * Types of statements.
347 * Those have `test` method to check it matches to the given statement.
348 * @private
349 */
350const StatementTypes = {
351    "*": { test: () => true },
352    "block-like": {
353        test: (node, sourceCode) => isBlockLikeStatement(sourceCode, node)
354    },
355    "cjs-export": {
356        test: (node, sourceCode) =>
357            node.type === "ExpressionStatement" &&
358            node.expression.type === "AssignmentExpression" &&
359            CJS_EXPORT.test(sourceCode.getText(node.expression.left))
360    },
361    "cjs-import": {
362        test: (node, sourceCode) =>
363            node.type === "VariableDeclaration" &&
364            node.declarations.length > 0 &&
365            Boolean(node.declarations[0].init) &&
366            CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init))
367    },
368    directive: {
369        test: isDirectivePrologue
370    },
371    expression: {
372        test: (node, sourceCode) =>
373            node.type === "ExpressionStatement" &&
374            !isDirectivePrologue(node, sourceCode)
375    },
376    iife: {
377        test: isIIFEStatement
378    },
379    "multiline-block-like": {
380        test: (node, sourceCode) =>
381            node.loc.start.line !== node.loc.end.line &&
382            isBlockLikeStatement(sourceCode, node)
383    },
384    "multiline-expression": {
385        test: (node, sourceCode) =>
386            node.loc.start.line !== node.loc.end.line &&
387            node.type === "ExpressionStatement" &&
388            !isDirectivePrologue(node, sourceCode)
389    },
390
391    "multiline-const": newMultilineKeywordTester("const"),
392    "multiline-let": newMultilineKeywordTester("let"),
393    "multiline-var": newMultilineKeywordTester("var"),
394    "singleline-const": newSinglelineKeywordTester("const"),
395    "singleline-let": newSinglelineKeywordTester("let"),
396    "singleline-var": newSinglelineKeywordTester("var"),
397
398    block: newNodeTypeTester("BlockStatement"),
399    empty: newNodeTypeTester("EmptyStatement"),
400    function: newNodeTypeTester("FunctionDeclaration"),
401
402    break: newKeywordTester("break"),
403    case: newKeywordTester("case"),
404    class: newKeywordTester("class"),
405    const: newKeywordTester("const"),
406    continue: newKeywordTester("continue"),
407    debugger: newKeywordTester("debugger"),
408    default: newKeywordTester("default"),
409    do: newKeywordTester("do"),
410    export: newKeywordTester("export"),
411    for: newKeywordTester("for"),
412    if: newKeywordTester("if"),
413    import: newKeywordTester("import"),
414    let: newKeywordTester("let"),
415    return: newKeywordTester("return"),
416    switch: newKeywordTester("switch"),
417    throw: newKeywordTester("throw"),
418    try: newKeywordTester("try"),
419    var: newKeywordTester("var"),
420    while: newKeywordTester("while"),
421    with: newKeywordTester("with")
422};
423
424//------------------------------------------------------------------------------
425// Rule Definition
426//------------------------------------------------------------------------------
427
428module.exports = {
429    meta: {
430        type: "layout",
431
432        docs: {
433            description: "require or disallow padding lines between statements",
434            category: "Stylistic Issues",
435            recommended: false,
436            url: "https://eslint.org/docs/rules/padding-line-between-statements"
437        },
438
439        fixable: "whitespace",
440
441        schema: {
442            definitions: {
443                paddingType: {
444                    enum: Object.keys(PaddingTypes)
445                },
446                statementType: {
447                    anyOf: [
448                        { enum: Object.keys(StatementTypes) },
449                        {
450                            type: "array",
451                            items: { enum: Object.keys(StatementTypes) },
452                            minItems: 1,
453                            uniqueItems: true,
454                            additionalItems: false
455                        }
456                    ]
457                }
458            },
459            type: "array",
460            items: {
461                type: "object",
462                properties: {
463                    blankLine: { $ref: "#/definitions/paddingType" },
464                    prev: { $ref: "#/definitions/statementType" },
465                    next: { $ref: "#/definitions/statementType" }
466                },
467                additionalProperties: false,
468                required: ["blankLine", "prev", "next"]
469            },
470            additionalItems: false
471        },
472
473        messages: {
474            unexpectedBlankLine: "Unexpected blank line before this statement.",
475            expectedBlankLine: "Expected blank line before this statement."
476        }
477    },
478
479    create(context) {
480        const sourceCode = context.getSourceCode();
481        const configureList = context.options || [];
482        let scopeInfo = null;
483
484        /**
485         * Processes to enter to new scope.
486         * This manages the current previous statement.
487         * @returns {void}
488         * @private
489         */
490        function enterScope() {
491            scopeInfo = {
492                upper: scopeInfo,
493                prevNode: null
494            };
495        }
496
497        /**
498         * Processes to exit from the current scope.
499         * @returns {void}
500         * @private
501         */
502        function exitScope() {
503            scopeInfo = scopeInfo.upper;
504        }
505
506        /**
507         * Checks whether the given node matches the given type.
508         * @param {ASTNode} node The statement node to check.
509         * @param {string|string[]} type The statement type to check.
510         * @returns {boolean} `true` if the statement node matched the type.
511         * @private
512         */
513        function match(node, type) {
514            let innerStatementNode = node;
515
516            while (innerStatementNode.type === "LabeledStatement") {
517                innerStatementNode = innerStatementNode.body;
518            }
519            if (Array.isArray(type)) {
520                return type.some(match.bind(null, innerStatementNode));
521            }
522            return StatementTypes[type].test(innerStatementNode, sourceCode);
523        }
524
525        /**
526         * Finds the last matched configure from configureList.
527         * @param {ASTNode} prevNode The previous statement to match.
528         * @param {ASTNode} nextNode The current statement to match.
529         * @returns {Object} The tester of the last matched configure.
530         * @private
531         */
532        function getPaddingType(prevNode, nextNode) {
533            for (let i = configureList.length - 1; i >= 0; --i) {
534                const configure = configureList[i];
535                const matched =
536                    match(prevNode, configure.prev) &&
537                    match(nextNode, configure.next);
538
539                if (matched) {
540                    return PaddingTypes[configure.blankLine];
541                }
542            }
543            return PaddingTypes.any;
544        }
545
546        /**
547         * Gets padding line sequences between the given 2 statements.
548         * Comments are separators of the padding line sequences.
549         * @param {ASTNode} prevNode The previous statement to count.
550         * @param {ASTNode} nextNode The current statement to count.
551         * @returns {Array<Token[]>} The array of token pairs.
552         * @private
553         */
554        function getPaddingLineSequences(prevNode, nextNode) {
555            const pairs = [];
556            let prevToken = getActualLastToken(sourceCode, prevNode);
557
558            if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
559                do {
560                    const token = sourceCode.getTokenAfter(
561                        prevToken,
562                        { includeComments: true }
563                    );
564
565                    if (token.loc.start.line - prevToken.loc.end.line >= 2) {
566                        pairs.push([prevToken, token]);
567                    }
568                    prevToken = token;
569
570                } while (prevToken.range[0] < nextNode.range[0]);
571            }
572
573            return pairs;
574        }
575
576        /**
577         * Verify padding lines between the given node and the previous node.
578         * @param {ASTNode} node The node to verify.
579         * @returns {void}
580         * @private
581         */
582        function verify(node) {
583            const parentType = node.parent.type;
584            const validParent =
585                astUtils.STATEMENT_LIST_PARENTS.has(parentType) ||
586                parentType === "SwitchStatement";
587
588            if (!validParent) {
589                return;
590            }
591
592            // Save this node as the current previous statement.
593            const prevNode = scopeInfo.prevNode;
594
595            // Verify.
596            if (prevNode) {
597                const type = getPaddingType(prevNode, node);
598                const paddingLines = getPaddingLineSequences(prevNode, node);
599
600                type.verify(context, prevNode, node, paddingLines);
601            }
602
603            scopeInfo.prevNode = node;
604        }
605
606        /**
607         * Verify padding lines between the given node and the previous node.
608         * Then process to enter to new scope.
609         * @param {ASTNode} node The node to verify.
610         * @returns {void}
611         * @private
612         */
613        function verifyThenEnterScope(node) {
614            verify(node);
615            enterScope();
616        }
617
618        return {
619            Program: enterScope,
620            BlockStatement: enterScope,
621            SwitchStatement: enterScope,
622            "Program:exit": exitScope,
623            "BlockStatement:exit": exitScope,
624            "SwitchStatement:exit": exitScope,
625
626            ":statement": verify,
627
628            SwitchCase: verifyThenEnterScope,
629            "SwitchCase:exit": exitScope
630        };
631    }
632};
633