UNPKG

11.5 kBJavaScriptView Raw
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, { skip: (node.async ? 1 : 0) });
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};