no-multiple-empty-lines.js 5.73 KB
Newer Older
YazhouChen's avatar
YazhouChen committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
/**
 * @fileoverview Disallows multiple blank lines.
 * implementation adapted from the no-trailing-spaces rule.
 * @author Greg Cochard
 */
"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
    meta: {
        docs: {
            description: "disallow multiple empty lines",
            category: "Stylistic Issues",
            recommended: false,
            url: "https://eslint.org/docs/rules/no-multiple-empty-lines"
        },

        fixable: "whitespace",

        schema: [
            {
                type: "object",
                properties: {
                    max: {
                        type: "integer",
                        minimum: 0
                    },
                    maxEOF: {
                        type: "integer",
                        minimum: 0
                    },
                    maxBOF: {
                        type: "integer",
                        minimum: 0
                    }
                },
                required: ["max"],
                additionalProperties: false
            }
        ]
    },

    create(context) {

        // Use options.max or 2 as default
        let max = 2,
            maxEOF = max,
            maxBOF = max;

        if (context.options.length) {
            max = context.options[0].max;
            maxEOF = typeof context.options[0].maxEOF !== "undefined" ? context.options[0].maxEOF : max;
            maxBOF = typeof context.options[0].maxBOF !== "undefined" ? context.options[0].maxBOF : max;
        }

        const sourceCode = context.getSourceCode();

        // Swallow the final newline, as some editors add it automatically and we don't want it to cause an issue
        const allLines = sourceCode.lines[sourceCode.lines.length - 1] === "" ? sourceCode.lines.slice(0, -1) : sourceCode.lines;
        const templateLiteralLines = new Set();

        //--------------------------------------------------------------------------
        // Public
        //--------------------------------------------------------------------------

        return {
            TemplateLiteral(node) {
                node.quasis.forEach(literalPart => {

                    // Empty lines have a semantic meaning if they're inside template literals. Don't count these as empty lines.
                    for (let ignoredLine = literalPart.loc.start.line; ignoredLine < literalPart.loc.end.line; ignoredLine++) {
                        templateLiteralLines.add(ignoredLine);
                    }
                });
            },
            "Program:exit"(node) {
                return allLines

                    // Given a list of lines, first get a list of line numbers that are non-empty.
                    .reduce((nonEmptyLineNumbers, line, index) => {
                        if (line.trim() || templateLiteralLines.has(index + 1)) {
                            nonEmptyLineNumbers.push(index + 1);
                        }
                        return nonEmptyLineNumbers;
                    }, [])

                    // Add a value at the end to allow trailing empty lines to be checked.
                    .concat(allLines.length + 1)

                    // Given two line numbers of non-empty lines, report the lines between if the difference is too large.
                    .reduce((lastLineNumber, lineNumber) => {
                        let message, maxAllowed;

                        if (lastLineNumber === 0) {
                            message = "Too many blank lines at the beginning of file. Max of {{max}} allowed.";
                            maxAllowed = maxBOF;
                        } else if (lineNumber === allLines.length + 1) {
                            message = "Too many blank lines at the end of file. Max of {{max}} allowed.";
                            maxAllowed = maxEOF;
                        } else {
                            message = "More than {{max}} blank {{pluralizedLines}} not allowed.";
                            maxAllowed = max;
                        }

                        if (lineNumber - lastLineNumber - 1 > maxAllowed) {
                            context.report({
                                node,
                                loc: { start: { line: lastLineNumber + 1, column: 0 }, end: { line: lineNumber, column: 0 } },
                                message,
                                data: { max: maxAllowed, pluralizedLines: maxAllowed === 1 ? "line" : "lines" },
                                fix(fixer) {
                                    const rangeStart = sourceCode.getIndexFromLoc({ line: lastLineNumber + 1, column: 0 });

                                    /*
                                     * The end of the removal range is usually the start index of the next line.
                                     * However, at the end of the file there is no next line, so the end of the
                                     * range is just the length of the text.
                                     */
                                    const lineNumberAfterRemovedLines = lineNumber - maxAllowed;
                                    const rangeEnd = lineNumberAfterRemovedLines <= allLines.length
                                        ? sourceCode.getIndexFromLoc({ line: lineNumberAfterRemovedLines, column: 0 })
                                        : sourceCode.text.length;

                                    return fixer.removeRange([rangeStart, rangeEnd]);
                                }
                            });
                        }

                        return lineNumber;
                    }, 0);
            }
        };
    }
};