| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632 | /** * @fileoverview Rule to require or disallow newlines between statements * @author Toru Nagashima */"use strict";//------------------------------------------------------------------------------// Requirements//------------------------------------------------------------------------------const astUtils = require("./utils/ast-utils");//------------------------------------------------------------------------------// Helpers//------------------------------------------------------------------------------const LT = `[${Array.from(astUtils.LINEBREAKS).join("")}]`;const PADDING_LINE_SEQUENCE = new RegExp(    String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`,    "u");const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u;const CJS_IMPORT = /^require\(/u;/** * Creates tester which check if a node starts with specific keyword. * @param {string} keyword The keyword to test. * @returns {Object} the created tester. * @private */function newKeywordTester(keyword) {    return {        test: (node, sourceCode) =>            sourceCode.getFirstToken(node).value === keyword    };}/** * Creates tester which check if a node starts with specific keyword and spans a single line. * @param {string} keyword The keyword to test. * @returns {Object} the created tester. * @private */function newSinglelineKeywordTester(keyword) {    return {        test: (node, sourceCode) =>            node.loc.start.line === node.loc.end.line &&            sourceCode.getFirstToken(node).value === keyword    };}/** * Creates tester which check if a node starts with specific keyword and spans multiple lines. * @param {string} keyword The keyword to test. * @returns {Object} the created tester. * @private */function newMultilineKeywordTester(keyword) {    return {        test: (node, sourceCode) =>            node.loc.start.line !== node.loc.end.line &&            sourceCode.getFirstToken(node).value === keyword    };}/** * Creates tester which check if a node is specific type. * @param {string} type The node type to test. * @returns {Object} the created tester. * @private */function newNodeTypeTester(type) {    return {        test: node =>            node.type === type    };}/** * Checks the given node is an expression statement of IIFE. * @param {ASTNode} node The node to check. * @returns {boolean} `true` if the node is an expression statement of IIFE. * @private */function isIIFEStatement(node) {    if (node.type === "ExpressionStatement") {        let call = astUtils.skipChainExpression(node.expression);        if (call.type === "UnaryExpression") {            call = astUtils.skipChainExpression(call.argument);        }        return call.type === "CallExpression" && astUtils.isFunction(call.callee);    }    return false;}/** * Checks whether the given node is a block-like statement. * This checks the last token of the node is the closing brace of a block. * @param {SourceCode} sourceCode The source code to get tokens. * @param {ASTNode} node The node to check. * @returns {boolean} `true` if the node is a block-like statement. * @private */function isBlockLikeStatement(sourceCode, node) {    // do-while with a block is a block-like statement.    if (node.type === "DoWhileStatement" && node.body.type === "BlockStatement") {        return true;    }    /*     * IIFE is a block-like statement specially from     * JSCS#disallowPaddingNewLinesAfterBlocks.     */    if (isIIFEStatement(node)) {        return true;    }    // Checks the last token is a closing brace of blocks.    const lastToken = sourceCode.getLastToken(node, astUtils.isNotSemicolonToken);    const belongingNode = lastToken && astUtils.isClosingBraceToken(lastToken)        ? sourceCode.getNodeByRangeIndex(lastToken.range[0])        : null;    return Boolean(belongingNode) && (        belongingNode.type === "BlockStatement" ||        belongingNode.type === "SwitchStatement"    );}/** * Check whether the given node is a directive or not. * @param {ASTNode} node The node to check. * @param {SourceCode} sourceCode The source code object to get tokens. * @returns {boolean} `true` if the node is a directive. */function isDirective(node, sourceCode) {    return (        node.type === "ExpressionStatement" &&        (            node.parent.type === "Program" ||            (                node.parent.type === "BlockStatement" &&                astUtils.isFunction(node.parent.parent)            )        ) &&        node.expression.type === "Literal" &&        typeof node.expression.value === "string" &&        !astUtils.isParenthesised(sourceCode, node.expression)    );}/** * Check whether the given node is a part of directive prologue or not. * @param {ASTNode} node The node to check. * @param {SourceCode} sourceCode The source code object to get tokens. * @returns {boolean} `true` if the node is a part of directive prologue. */function isDirectivePrologue(node, sourceCode) {    if (isDirective(node, sourceCode)) {        for (const sibling of node.parent.body) {            if (sibling === node) {                break;            }            if (!isDirective(sibling, sourceCode)) {                return false;            }        }        return true;    }    return false;}/** * Gets the actual last token. * * If a semicolon is semicolon-less style's semicolon, this ignores it. * For example: * *     foo() *     ;[1, 2, 3].forEach(bar) * @param {SourceCode} sourceCode The source code to get tokens. * @param {ASTNode} node The node to get. * @returns {Token} The actual last token. * @private */function getActualLastToken(sourceCode, node) {    const semiToken = sourceCode.getLastToken(node);    const prevToken = sourceCode.getTokenBefore(semiToken);    const nextToken = sourceCode.getTokenAfter(semiToken);    const isSemicolonLessStyle = Boolean(        prevToken &&        nextToken &&        prevToken.range[0] >= node.range[0] &&        astUtils.isSemicolonToken(semiToken) &&        semiToken.loc.start.line !== prevToken.loc.end.line &&        semiToken.loc.end.line === nextToken.loc.start.line    );    return isSemicolonLessStyle ? prevToken : semiToken;}/** * This returns the concatenation of the first 2 captured strings. * @param {string} _ Unused. Whole matched string. * @param {string} trailingSpaces The trailing spaces of the first line. * @param {string} indentSpaces The indentation spaces of the last line. * @returns {string} The concatenation of trailingSpaces and indentSpaces. * @private */function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) {    return trailingSpaces + indentSpaces;}/** * Check and report statements for `any` configuration. * It does nothing. * @returns {void} * @private */function verifyForAny() {}/** * Check and report statements for `never` configuration. * This autofix removes blank lines between the given 2 statements. * However, if comments exist between 2 blank lines, it does not remove those * blank lines automatically. * @param {RuleContext} context The rule context to report. * @param {ASTNode} _ Unused. The previous node to check. * @param {ASTNode} nextNode The next node to check. * @param {Array<Token[]>} paddingLines The array of token pairs that blank * lines exist between the pair. * @returns {void} * @private */function verifyForNever(context, _, nextNode, paddingLines) {    if (paddingLines.length === 0) {        return;    }    context.report({        node: nextNode,        messageId: "unexpectedBlankLine",        fix(fixer) {            if (paddingLines.length >= 2) {                return null;            }            const prevToken = paddingLines[0][0];            const nextToken = paddingLines[0][1];            const start = prevToken.range[1];            const end = nextToken.range[0];            const text = context.getSourceCode().text                .slice(start, end)                .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines);            return fixer.replaceTextRange([start, end], text);        }    });}/** * Check and report statements for `always` configuration. * This autofix inserts a blank line between the given 2 statements. * If the `prevNode` has trailing comments, it inserts a blank line after the * trailing comments. * @param {RuleContext} context The rule context to report. * @param {ASTNode} prevNode The previous node to check. * @param {ASTNode} nextNode The next node to check. * @param {Array<Token[]>} paddingLines The array of token pairs that blank * lines exist between the pair. * @returns {void} * @private */function verifyForAlways(context, prevNode, nextNode, paddingLines) {    if (paddingLines.length > 0) {        return;    }    context.report({        node: nextNode,        messageId: "expectedBlankLine",        fix(fixer) {            const sourceCode = context.getSourceCode();            let prevToken = getActualLastToken(sourceCode, prevNode);            const nextToken = sourceCode.getFirstTokenBetween(                prevToken,                nextNode,                {                    includeComments: true,                    /**                     * Skip the trailing comments of the previous node.                     * This inserts a blank line after the last trailing comment.                     *                     * For example:                     *                     *     foo(); // trailing comment.                     *     // comment.                     *     bar();                     *                     * Get fixed to:                     *                     *     foo(); // trailing comment.                     *                     *     // comment.                     *     bar();                     * @param {Token} token The token to check.                     * @returns {boolean} `true` if the token is not a trailing comment.                     * @private                     */                    filter(token) {                        if (astUtils.isTokenOnSameLine(prevToken, token)) {                            prevToken = token;                            return false;                        }                        return true;                    }                }            ) || nextNode;            const insertText = astUtils.isTokenOnSameLine(prevToken, nextToken)                ? "\n\n"                : "\n";            return fixer.insertTextAfter(prevToken, insertText);        }    });}/** * Types of blank lines. * `any`, `never`, and `always` are defined. * Those have `verify` method to check and report statements. * @private */const PaddingTypes = {    any: { verify: verifyForAny },    never: { verify: verifyForNever },    always: { verify: verifyForAlways }};/** * Types of statements. * Those have `test` method to check it matches to the given statement. * @private */const StatementTypes = {    "*": { test: () => true },    "block-like": {        test: (node, sourceCode) => isBlockLikeStatement(sourceCode, node)    },    "cjs-export": {        test: (node, sourceCode) =>            node.type === "ExpressionStatement" &&            node.expression.type === "AssignmentExpression" &&            CJS_EXPORT.test(sourceCode.getText(node.expression.left))    },    "cjs-import": {        test: (node, sourceCode) =>            node.type === "VariableDeclaration" &&            node.declarations.length > 0 &&            Boolean(node.declarations[0].init) &&            CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init))    },    directive: {        test: isDirectivePrologue    },    expression: {        test: (node, sourceCode) =>            node.type === "ExpressionStatement" &&            !isDirectivePrologue(node, sourceCode)    },    iife: {        test: isIIFEStatement    },    "multiline-block-like": {        test: (node, sourceCode) =>            node.loc.start.line !== node.loc.end.line &&            isBlockLikeStatement(sourceCode, node)    },    "multiline-expression": {        test: (node, sourceCode) =>            node.loc.start.line !== node.loc.end.line &&            node.type === "ExpressionStatement" &&            !isDirectivePrologue(node, sourceCode)    },    "multiline-const": newMultilineKeywordTester("const"),    "multiline-let": newMultilineKeywordTester("let"),    "multiline-var": newMultilineKeywordTester("var"),    "singleline-const": newSinglelineKeywordTester("const"),    "singleline-let": newSinglelineKeywordTester("let"),    "singleline-var": newSinglelineKeywordTester("var"),    block: newNodeTypeTester("BlockStatement"),    empty: newNodeTypeTester("EmptyStatement"),    function: newNodeTypeTester("FunctionDeclaration"),    break: newKeywordTester("break"),    case: newKeywordTester("case"),    class: newKeywordTester("class"),    const: newKeywordTester("const"),    continue: newKeywordTester("continue"),    debugger: newKeywordTester("debugger"),    default: newKeywordTester("default"),    do: newKeywordTester("do"),    export: newKeywordTester("export"),    for: newKeywordTester("for"),    if: newKeywordTester("if"),    import: newKeywordTester("import"),    let: newKeywordTester("let"),    return: newKeywordTester("return"),    switch: newKeywordTester("switch"),    throw: newKeywordTester("throw"),    try: newKeywordTester("try"),    var: newKeywordTester("var"),    while: newKeywordTester("while"),    with: newKeywordTester("with")};//------------------------------------------------------------------------------// Rule Definition//------------------------------------------------------------------------------/** @type {import('../shared/types').Rule} */module.exports = {    meta: {        type: "layout",        docs: {            description: "Require or disallow padding lines between statements",            recommended: false,            url: "https://eslint.org/docs/rules/padding-line-between-statements"        },        fixable: "whitespace",        schema: {            definitions: {                paddingType: {                    enum: Object.keys(PaddingTypes)                },                statementType: {                    anyOf: [                        { enum: Object.keys(StatementTypes) },                        {                            type: "array",                            items: { enum: Object.keys(StatementTypes) },                            minItems: 1,                            uniqueItems: true                        }                    ]                }            },            type: "array",            items: {                type: "object",                properties: {                    blankLine: { $ref: "#/definitions/paddingType" },                    prev: { $ref: "#/definitions/statementType" },                    next: { $ref: "#/definitions/statementType" }                },                additionalProperties: false,                required: ["blankLine", "prev", "next"]            }        },        messages: {            unexpectedBlankLine: "Unexpected blank line before this statement.",            expectedBlankLine: "Expected blank line before this statement."        }    },    create(context) {        const sourceCode = context.getSourceCode();        const configureList = context.options || [];        let scopeInfo = null;        /**         * Processes to enter to new scope.         * This manages the current previous statement.         * @returns {void}         * @private         */        function enterScope() {            scopeInfo = {                upper: scopeInfo,                prevNode: null            };        }        /**         * Processes to exit from the current scope.         * @returns {void}         * @private         */        function exitScope() {            scopeInfo = scopeInfo.upper;        }        /**         * Checks whether the given node matches the given type.         * @param {ASTNode} node The statement node to check.         * @param {string|string[]} type The statement type to check.         * @returns {boolean} `true` if the statement node matched the type.         * @private         */        function match(node, type) {            let innerStatementNode = node;            while (innerStatementNode.type === "LabeledStatement") {                innerStatementNode = innerStatementNode.body;            }            if (Array.isArray(type)) {                return type.some(match.bind(null, innerStatementNode));            }            return StatementTypes[type].test(innerStatementNode, sourceCode);        }        /**         * Finds the last matched configure from configureList.         * @param {ASTNode} prevNode The previous statement to match.         * @param {ASTNode} nextNode The current statement to match.         * @returns {Object} The tester of the last matched configure.         * @private         */        function getPaddingType(prevNode, nextNode) {            for (let i = configureList.length - 1; i >= 0; --i) {                const configure = configureList[i];                const matched =                    match(prevNode, configure.prev) &&                    match(nextNode, configure.next);                if (matched) {                    return PaddingTypes[configure.blankLine];                }            }            return PaddingTypes.any;        }        /**         * Gets padding line sequences between the given 2 statements.         * Comments are separators of the padding line sequences.         * @param {ASTNode} prevNode The previous statement to count.         * @param {ASTNode} nextNode The current statement to count.         * @returns {Array<Token[]>} The array of token pairs.         * @private         */        function getPaddingLineSequences(prevNode, nextNode) {            const pairs = [];            let prevToken = getActualLastToken(sourceCode, prevNode);            if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {                do {                    const token = sourceCode.getTokenAfter(                        prevToken,                        { includeComments: true }                    );                    if (token.loc.start.line - prevToken.loc.end.line >= 2) {                        pairs.push([prevToken, token]);                    }                    prevToken = token;                } while (prevToken.range[0] < nextNode.range[0]);            }            return pairs;        }        /**         * Verify padding lines between the given node and the previous node.         * @param {ASTNode} node The node to verify.         * @returns {void}         * @private         */        function verify(node) {            const parentType = node.parent.type;            const validParent =                astUtils.STATEMENT_LIST_PARENTS.has(parentType) ||                parentType === "SwitchStatement";            if (!validParent) {                return;            }            // Save this node as the current previous statement.            const prevNode = scopeInfo.prevNode;            // Verify.            if (prevNode) {                const type = getPaddingType(prevNode, node);                const paddingLines = getPaddingLineSequences(prevNode, node);                type.verify(context, prevNode, node, paddingLines);            }            scopeInfo.prevNode = node;        }        /**         * Verify padding lines between the given node and the previous node.         * Then process to enter to new scope.         * @param {ASTNode} node The node to verify.         * @returns {void}         * @private         */        function verifyThenEnterScope(node) {            verify(node);            enterScope();        }        return {            Program: enterScope,            BlockStatement: enterScope,            SwitchStatement: enterScope,            StaticBlock: enterScope,            "Program:exit": exitScope,            "BlockStatement:exit": exitScope,            "SwitchStatement:exit": exitScope,            "StaticBlock:exit": exitScope,            ":statement": verify,            SwitchCase: verifyThenEnterScope,            "SwitchCase:exit": exitScope        };    }};
 |