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