| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 | /** * @fileoverview enforce consistent line breaks inside function parentheses * @author Teddy Katz */"use strict";//------------------------------------------------------------------------------// Requirements//------------------------------------------------------------------------------const astUtils = require("./utils/ast-utils");//------------------------------------------------------------------------------// Rule Definition//------------------------------------------------------------------------------/** @type {import('../shared/types').Rule} */module.exports = {    meta: {        type: "layout",        docs: {            description: "Enforce consistent line breaks inside function parentheses",            recommended: false,            url: "https://eslint.org/docs/rules/function-paren-newline"        },        fixable: "whitespace",        schema: [            {                oneOf: [                    {                        enum: ["always", "never", "consistent", "multiline", "multiline-arguments"]                    },                    {                        type: "object",                        properties: {                            minItems: {                                type: "integer",                                minimum: 0                            }                        },                        additionalProperties: false                    }                ]            }        ],        messages: {            expectedBefore: "Expected newline before ')'.",            expectedAfter: "Expected newline after '('.",            expectedBetween: "Expected newline between arguments/params.",            unexpectedBefore: "Unexpected newline before ')'.",            unexpectedAfter: "Unexpected newline after '('."        }    },    create(context) {        const sourceCode = context.getSourceCode();        const rawOption = context.options[0] || "multiline";        const multilineOption = rawOption === "multiline";        const multilineArgumentsOption = rawOption === "multiline-arguments";        const consistentOption = rawOption === "consistent";        let minItems;        if (typeof rawOption === "object") {            minItems = rawOption.minItems;        } else if (rawOption === "always") {            minItems = 0;        } else if (rawOption === "never") {            minItems = Infinity;        } else {            minItems = null;        }        //----------------------------------------------------------------------        // Helpers        //----------------------------------------------------------------------        /**         * Determines whether there should be newlines inside function parens         * @param {ASTNode[]} elements The arguments or parameters in the list         * @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code.         * @returns {boolean} `true` if there should be newlines inside the function parens         */        function shouldHaveNewlines(elements, hasLeftNewline) {            if (multilineArgumentsOption && elements.length === 1) {                return hasLeftNewline;            }            if (multilineOption || multilineArgumentsOption) {                return elements.some((element, index) => index !== elements.length - 1 && element.loc.end.line !== elements[index + 1].loc.start.line);            }            if (consistentOption) {                return hasLeftNewline;            }            return elements.length >= minItems;        }        /**         * Validates parens         * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token         * @param {ASTNode[]} elements The arguments or parameters in the list         * @returns {void}         */        function validateParens(parens, elements) {            const leftParen = parens.leftParen;            const rightParen = parens.rightParen;            const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);            const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen);            const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);            const hasRightNewline = !astUtils.isTokenOnSameLine(tokenBeforeRightParen, rightParen);            const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);            if (hasLeftNewline && !needsNewlines) {                context.report({                    node: leftParen,                    messageId: "unexpectedAfter",                    fix(fixer) {                        return sourceCode.getText().slice(leftParen.range[1], tokenAfterLeftParen.range[0]).trim()                            // If there is a comment between the ( and the first element, don't do a fix.                            ? null                            : fixer.removeRange([leftParen.range[1], tokenAfterLeftParen.range[0]]);                    }                });            } else if (!hasLeftNewline && needsNewlines) {                context.report({                    node: leftParen,                    messageId: "expectedAfter",                    fix: fixer => fixer.insertTextAfter(leftParen, "\n")                });            }            if (hasRightNewline && !needsNewlines) {                context.report({                    node: rightParen,                    messageId: "unexpectedBefore",                    fix(fixer) {                        return sourceCode.getText().slice(tokenBeforeRightParen.range[1], rightParen.range[0]).trim()                            // If there is a comment between the last element and the ), don't do a fix.                            ? null                            : fixer.removeRange([tokenBeforeRightParen.range[1], rightParen.range[0]]);                    }                });            } else if (!hasRightNewline && needsNewlines) {                context.report({                    node: rightParen,                    messageId: "expectedBefore",                    fix: fixer => fixer.insertTextBefore(rightParen, "\n")                });            }        }        /**         * Validates a list of arguments or parameters         * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token         * @param {ASTNode[]} elements The arguments or parameters in the list         * @returns {void}         */        function validateArguments(parens, elements) {            const leftParen = parens.leftParen;            const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);            const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);            const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);            for (let i = 0; i <= elements.length - 2; i++) {                const currentElement = elements[i];                const nextElement = elements[i + 1];                const hasNewLine = currentElement.loc.end.line !== nextElement.loc.start.line;                if (!hasNewLine && needsNewlines) {                    context.report({                        node: currentElement,                        messageId: "expectedBetween",                        fix: fixer => fixer.insertTextBefore(nextElement, "\n")                    });                }            }        }        /**         * Gets the left paren and right paren tokens of a node.         * @param {ASTNode} node The node with parens         * @throws {TypeError} Unexpected node type.         * @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token.         * Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression         * with a single parameter)         */        function getParenTokens(node) {            switch (node.type) {                case "NewExpression":                    if (!node.arguments.length &&                        !(                            astUtils.isOpeningParenToken(sourceCode.getLastToken(node, { skip: 1 })) &&                            astUtils.isClosingParenToken(sourceCode.getLastToken(node)) &&                            node.callee.range[1] < node.range[1]                        )                    ) {                        // If the NewExpression does not have parens (e.g. `new Foo`), return null.                        return null;                    }                    // falls through                case "CallExpression":                    return {                        leftParen: sourceCode.getTokenAfter(node.callee, astUtils.isOpeningParenToken),                        rightParen: sourceCode.getLastToken(node)                    };                case "FunctionDeclaration":                case "FunctionExpression": {                    const leftParen = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken);                    const rightParen = node.params.length                        ? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken)                        : sourceCode.getTokenAfter(leftParen);                    return { leftParen, rightParen };                }                case "ArrowFunctionExpression": {                    const firstToken = sourceCode.getFirstToken(node, { skip: (node.async ? 1 : 0) });                    if (!astUtils.isOpeningParenToken(firstToken)) {                        // If the ArrowFunctionExpression has a single param without parens, return null.                        return null;                    }                    const rightParen = node.params.length                        ? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken)                        : sourceCode.getTokenAfter(firstToken);                    return {                        leftParen: firstToken,                        rightParen                    };                }                case "ImportExpression": {                    const leftParen = sourceCode.getFirstToken(node, 1);                    const rightParen = sourceCode.getLastToken(node);                    return { leftParen, rightParen };                }                default:                    throw new TypeError(`unexpected node with type ${node.type}`);            }        }        //----------------------------------------------------------------------        // Public        //----------------------------------------------------------------------        return {            [[                "ArrowFunctionExpression",                "CallExpression",                "FunctionDeclaration",                "FunctionExpression",                "ImportExpression",                "NewExpression"            ]](node) {                const parens = getParenTokens(node);                let params;                if (node.type === "ImportExpression") {                    params = [node.source];                } else if (astUtils.isFunction(node)) {                    params = node.params;                } else {                    params = node.arguments;                }                if (parens) {                    validateParens(parens, params);                    if (multilineArgumentsOption) {                        validateArguments(parens, params);                    }                }            }        };    }};
 |