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