• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview enforce consistent line breaks inside function parentheses
3 * @author Teddy Katz
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Requirements
9//------------------------------------------------------------------------------
10
11const astUtils = require("./utils/ast-utils");
12
13//------------------------------------------------------------------------------
14// Rule Definition
15//------------------------------------------------------------------------------
16
17module.exports = {
18    meta: {
19        type: "layout",
20
21        docs: {
22            description: "enforce consistent line breaks inside function parentheses",
23            category: "Stylistic Issues",
24            recommended: false,
25            url: "https://eslint.org/docs/rules/function-paren-newline"
26        },
27
28        fixable: "whitespace",
29
30        schema: [
31            {
32                oneOf: [
33                    {
34                        enum: ["always", "never", "consistent", "multiline", "multiline-arguments"]
35                    },
36                    {
37                        type: "object",
38                        properties: {
39                            minItems: {
40                                type: "integer",
41                                minimum: 0
42                            }
43                        },
44                        additionalProperties: false
45                    }
46                ]
47            }
48        ],
49
50        messages: {
51            expectedBefore: "Expected newline before ')'.",
52            expectedAfter: "Expected newline after '('.",
53            expectedBetween: "Expected newline between arguments/params.",
54            unexpectedBefore: "Unexpected newline before ')'.",
55            unexpectedAfter: "Unexpected newline after '('."
56        }
57    },
58
59    create(context) {
60        const sourceCode = context.getSourceCode();
61        const rawOption = context.options[0] || "multiline";
62        const multilineOption = rawOption === "multiline";
63        const multilineArgumentsOption = rawOption === "multiline-arguments";
64        const consistentOption = rawOption === "consistent";
65        let minItems;
66
67        if (typeof rawOption === "object") {
68            minItems = rawOption.minItems;
69        } else if (rawOption === "always") {
70            minItems = 0;
71        } else if (rawOption === "never") {
72            minItems = Infinity;
73        } else {
74            minItems = null;
75        }
76
77        //----------------------------------------------------------------------
78        // Helpers
79        //----------------------------------------------------------------------
80
81        /**
82         * Determines whether there should be newlines inside function parens
83         * @param {ASTNode[]} elements The arguments or parameters in the list
84         * @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code.
85         * @returns {boolean} `true` if there should be newlines inside the function parens
86         */
87        function shouldHaveNewlines(elements, hasLeftNewline) {
88            if (multilineArgumentsOption && elements.length === 1) {
89                return hasLeftNewline;
90            }
91            if (multilineOption || multilineArgumentsOption) {
92                return elements.some((element, index) => index !== elements.length - 1 && element.loc.end.line !== elements[index + 1].loc.start.line);
93            }
94            if (consistentOption) {
95                return hasLeftNewline;
96            }
97            return elements.length >= minItems;
98        }
99
100        /**
101         * Validates parens
102         * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
103         * @param {ASTNode[]} elements The arguments or parameters in the list
104         * @returns {void}
105         */
106        function validateParens(parens, elements) {
107            const leftParen = parens.leftParen;
108            const rightParen = parens.rightParen;
109            const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
110            const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen);
111            const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);
112            const hasRightNewline = !astUtils.isTokenOnSameLine(tokenBeforeRightParen, rightParen);
113            const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
114
115            if (hasLeftNewline && !needsNewlines) {
116                context.report({
117                    node: leftParen,
118                    messageId: "unexpectedAfter",
119                    fix(fixer) {
120                        return sourceCode.getText().slice(leftParen.range[1], tokenAfterLeftParen.range[0]).trim()
121
122                            // If there is a comment between the ( and the first element, don't do a fix.
123                            ? null
124                            : fixer.removeRange([leftParen.range[1], tokenAfterLeftParen.range[0]]);
125                    }
126                });
127            } else if (!hasLeftNewline && needsNewlines) {
128                context.report({
129                    node: leftParen,
130                    messageId: "expectedAfter",
131                    fix: fixer => fixer.insertTextAfter(leftParen, "\n")
132                });
133            }
134
135            if (hasRightNewline && !needsNewlines) {
136                context.report({
137                    node: rightParen,
138                    messageId: "unexpectedBefore",
139                    fix(fixer) {
140                        return sourceCode.getText().slice(tokenBeforeRightParen.range[1], rightParen.range[0]).trim()
141
142                            // If there is a comment between the last element and the ), don't do a fix.
143                            ? null
144                            : fixer.removeRange([tokenBeforeRightParen.range[1], rightParen.range[0]]);
145                    }
146                });
147            } else if (!hasRightNewline && needsNewlines) {
148                context.report({
149                    node: rightParen,
150                    messageId: "expectedBefore",
151                    fix: fixer => fixer.insertTextBefore(rightParen, "\n")
152                });
153            }
154        }
155
156        /**
157         * Validates a list of arguments or parameters
158         * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
159         * @param {ASTNode[]} elements The arguments or parameters in the list
160         * @returns {void}
161         */
162        function validateArguments(parens, elements) {
163            const leftParen = parens.leftParen;
164            const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
165            const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);
166            const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
167
168            for (let i = 0; i <= elements.length - 2; i++) {
169                const currentElement = elements[i];
170                const nextElement = elements[i + 1];
171                const hasNewLine = currentElement.loc.end.line !== nextElement.loc.start.line;
172
173                if (!hasNewLine && needsNewlines) {
174                    context.report({
175                        node: currentElement,
176                        messageId: "expectedBetween",
177                        fix: fixer => fixer.insertTextBefore(nextElement, "\n")
178                    });
179                }
180            }
181        }
182
183        /**
184         * Gets the left paren and right paren tokens of a node.
185         * @param {ASTNode} node The node with parens
186         * @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token.
187         * Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression
188         * with a single parameter)
189         */
190        function getParenTokens(node) {
191            switch (node.type) {
192                case "NewExpression":
193                    if (!node.arguments.length && !(
194                        astUtils.isOpeningParenToken(sourceCode.getLastToken(node, { skip: 1 })) &&
195                        astUtils.isClosingParenToken(sourceCode.getLastToken(node))
196                    )) {
197
198                        // If the NewExpression does not have parens (e.g. `new Foo`), return null.
199                        return null;
200                    }
201
202                    // falls through
203
204                case "CallExpression":
205                    return {
206                        leftParen: sourceCode.getTokenAfter(node.callee, astUtils.isOpeningParenToken),
207                        rightParen: sourceCode.getLastToken(node)
208                    };
209
210                case "FunctionDeclaration":
211                case "FunctionExpression": {
212                    const leftParen = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken);
213                    const rightParen = node.params.length
214                        ? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken)
215                        : sourceCode.getTokenAfter(leftParen);
216
217                    return { leftParen, rightParen };
218                }
219
220                case "ArrowFunctionExpression": {
221                    const firstToken = sourceCode.getFirstToken(node);
222
223                    if (!astUtils.isOpeningParenToken(firstToken)) {
224
225                        // If the ArrowFunctionExpression has a single param without parens, return null.
226                        return null;
227                    }
228
229                    return {
230                        leftParen: firstToken,
231                        rightParen: sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken)
232                    };
233                }
234
235                case "ImportExpression": {
236                    const leftParen = sourceCode.getFirstToken(node, 1);
237                    const rightParen = sourceCode.getLastToken(node);
238
239                    return { leftParen, rightParen };
240                }
241
242                default:
243                    throw new TypeError(`unexpected node with type ${node.type}`);
244            }
245        }
246
247        //----------------------------------------------------------------------
248        // Public
249        //----------------------------------------------------------------------
250
251        return {
252            [[
253                "ArrowFunctionExpression",
254                "CallExpression",
255                "FunctionDeclaration",
256                "FunctionExpression",
257                "ImportExpression",
258                "NewExpression"
259            ]](node) {
260                const parens = getParenTokens(node);
261                let params;
262
263                if (node.type === "ImportExpression") {
264                    params = [node.source];
265                } else if (astUtils.isFunction(node)) {
266                    params = node.params;
267                } else {
268                    params = node.arguments;
269                }
270
271                if (parens) {
272                    validateParens(parens, params);
273
274                    if (multilineArgumentsOption) {
275                        validateArguments(parens, params);
276                    }
277                }
278            }
279        };
280    }
281};
282