1/** 2 * @fileoverview Rule to require braces in arrow function body. 3 * @author Alberto Rodríguez 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: "suggestion", 20 21 docs: { 22 description: "require braces around arrow function bodies", 23 category: "ECMAScript 6", 24 recommended: false, 25 url: "https://eslint.org/docs/rules/arrow-body-style" 26 }, 27 28 schema: { 29 anyOf: [ 30 { 31 type: "array", 32 items: [ 33 { 34 enum: ["always", "never"] 35 } 36 ], 37 minItems: 0, 38 maxItems: 1 39 }, 40 { 41 type: "array", 42 items: [ 43 { 44 enum: ["as-needed"] 45 }, 46 { 47 type: "object", 48 properties: { 49 requireReturnForObjectLiteral: { type: "boolean" } 50 }, 51 additionalProperties: false 52 } 53 ], 54 minItems: 0, 55 maxItems: 2 56 } 57 ] 58 }, 59 60 fixable: "code", 61 62 messages: { 63 unexpectedOtherBlock: "Unexpected block statement surrounding arrow body.", 64 unexpectedEmptyBlock: "Unexpected block statement surrounding arrow body; put a value of `undefined` immediately after the `=>`.", 65 unexpectedObjectBlock: "Unexpected block statement surrounding arrow body; parenthesize the returned value and move it immediately after the `=>`.", 66 unexpectedSingleBlock: "Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.", 67 expectedBlock: "Expected block statement surrounding arrow body." 68 } 69 }, 70 71 create(context) { 72 const options = context.options; 73 const always = options[0] === "always"; 74 const asNeeded = !options[0] || options[0] === "as-needed"; 75 const never = options[0] === "never"; 76 const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral; 77 const sourceCode = context.getSourceCode(); 78 let funcInfo = null; 79 80 /** 81 * Checks whether the given node has ASI problem or not. 82 * @param {Token} token The token to check. 83 * @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed. 84 */ 85 function hasASIProblem(token) { 86 return token && token.type === "Punctuator" && /^[([/`+-]/u.test(token.value); 87 } 88 89 /** 90 * Gets the closing parenthesis which is the pair of the given opening parenthesis. 91 * @param {Token} token The opening parenthesis token to get. 92 * @returns {Token} The found closing parenthesis token. 93 */ 94 function findClosingParen(token) { 95 let node = sourceCode.getNodeByRangeIndex(token.range[0]); 96 97 while (!astUtils.isParenthesised(sourceCode, node)) { 98 node = node.parent; 99 } 100 return sourceCode.getTokenAfter(node); 101 } 102 103 /** 104 * Check whether the node is inside of a for loop's init 105 * @param {ASTNode} node node is inside for loop 106 * @returns {boolean} `true` if the node is inside of a for loop, else `false` 107 */ 108 function isInsideForLoopInitializer(node) { 109 if (node && node.parent) { 110 if (node.parent.type === "ForStatement" && node.parent.init === node) { 111 return true; 112 } 113 return isInsideForLoopInitializer(node.parent); 114 } 115 return false; 116 } 117 118 /** 119 * Determines whether a arrow function body needs braces 120 * @param {ASTNode} node The arrow function node. 121 * @returns {void} 122 */ 123 function validate(node) { 124 const arrowBody = node.body; 125 126 if (arrowBody.type === "BlockStatement") { 127 const blockBody = arrowBody.body; 128 129 if (blockBody.length !== 1 && !never) { 130 return; 131 } 132 133 if (asNeeded && requireReturnForObjectLiteral && blockBody[0].type === "ReturnStatement" && 134 blockBody[0].argument && blockBody[0].argument.type === "ObjectExpression") { 135 return; 136 } 137 138 if (never || asNeeded && blockBody[0].type === "ReturnStatement") { 139 let messageId; 140 141 if (blockBody.length === 0) { 142 messageId = "unexpectedEmptyBlock"; 143 } else if (blockBody.length > 1) { 144 messageId = "unexpectedOtherBlock"; 145 } else if (blockBody[0].argument === null) { 146 messageId = "unexpectedSingleBlock"; 147 } else if (astUtils.isOpeningBraceToken(sourceCode.getFirstToken(blockBody[0], { skip: 1 }))) { 148 messageId = "unexpectedObjectBlock"; 149 } else { 150 messageId = "unexpectedSingleBlock"; 151 } 152 153 context.report({ 154 node, 155 loc: arrowBody.loc.start, 156 messageId, 157 fix(fixer) { 158 const fixes = []; 159 160 if (blockBody.length !== 1 || 161 blockBody[0].type !== "ReturnStatement" || 162 !blockBody[0].argument || 163 hasASIProblem(sourceCode.getTokenAfter(arrowBody)) 164 ) { 165 return fixes; 166 } 167 168 const openingBrace = sourceCode.getFirstToken(arrowBody); 169 const closingBrace = sourceCode.getLastToken(arrowBody); 170 const firstValueToken = sourceCode.getFirstToken(blockBody[0], 1); 171 const lastValueToken = sourceCode.getLastToken(blockBody[0]); 172 const commentsExist = 173 sourceCode.commentsExistBetween(openingBrace, firstValueToken) || 174 sourceCode.commentsExistBetween(lastValueToken, closingBrace); 175 176 /* 177 * Remove tokens around the return value. 178 * If comments don't exist, remove extra spaces as well. 179 */ 180 if (commentsExist) { 181 fixes.push( 182 fixer.remove(openingBrace), 183 fixer.remove(closingBrace), 184 fixer.remove(sourceCode.getTokenAfter(openingBrace)) // return keyword 185 ); 186 } else { 187 fixes.push( 188 fixer.removeRange([openingBrace.range[0], firstValueToken.range[0]]), 189 fixer.removeRange([lastValueToken.range[1], closingBrace.range[1]]) 190 ); 191 } 192 193 /* 194 * If the first token of the reutrn value is `{` or the return value is a sequence expression, 195 * enclose the return value by parentheses to avoid syntax error. 196 */ 197 if (astUtils.isOpeningBraceToken(firstValueToken) || blockBody[0].argument.type === "SequenceExpression" || (funcInfo.hasInOperator && isInsideForLoopInitializer(node))) { 198 if (!astUtils.isParenthesised(sourceCode, blockBody[0].argument)) { 199 fixes.push( 200 fixer.insertTextBefore(firstValueToken, "("), 201 fixer.insertTextAfter(lastValueToken, ")") 202 ); 203 } 204 } 205 206 /* 207 * If the last token of the return statement is semicolon, remove it. 208 * Non-block arrow body is an expression, not a statement. 209 */ 210 if (astUtils.isSemicolonToken(lastValueToken)) { 211 fixes.push(fixer.remove(lastValueToken)); 212 } 213 214 return fixes; 215 } 216 }); 217 } 218 } else { 219 if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) { 220 context.report({ 221 node, 222 loc: arrowBody.loc.start, 223 messageId: "expectedBlock", 224 fix(fixer) { 225 const fixes = []; 226 const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken); 227 const [firstTokenAfterArrow, secondTokenAfterArrow] = sourceCode.getTokensAfter(arrowToken, { count: 2 }); 228 const lastToken = sourceCode.getLastToken(node); 229 const isParenthesisedObjectLiteral = 230 astUtils.isOpeningParenToken(firstTokenAfterArrow) && 231 astUtils.isOpeningBraceToken(secondTokenAfterArrow); 232 233 // If the value is object literal, remove parentheses which were forced by syntax. 234 if (isParenthesisedObjectLiteral) { 235 const openingParenToken = firstTokenAfterArrow; 236 const openingBraceToken = secondTokenAfterArrow; 237 238 if (astUtils.isTokenOnSameLine(openingParenToken, openingBraceToken)) { 239 fixes.push(fixer.replaceText(openingParenToken, "{return ")); 240 } else { 241 242 // Avoid ASI 243 fixes.push( 244 fixer.replaceText(openingParenToken, "{"), 245 fixer.insertTextBefore(openingBraceToken, "return ") 246 ); 247 } 248 249 // Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo() 250 fixes.push(fixer.remove(findClosingParen(openingBraceToken))); 251 fixes.push(fixer.insertTextAfter(lastToken, "}")); 252 253 } else { 254 fixes.push(fixer.insertTextBefore(firstTokenAfterArrow, "{return ")); 255 fixes.push(fixer.insertTextAfter(lastToken, "}")); 256 } 257 258 return fixes; 259 } 260 }); 261 } 262 } 263 } 264 265 return { 266 "BinaryExpression[operator='in']"() { 267 let info = funcInfo; 268 269 while (info) { 270 info.hasInOperator = true; 271 info = info.upper; 272 } 273 }, 274 ArrowFunctionExpression() { 275 funcInfo = { 276 upper: funcInfo, 277 hasInOperator: false 278 }; 279 }, 280 "ArrowFunctionExpression:exit"(node) { 281 validate(node); 282 funcInfo = funcInfo.upper; 283 } 284 }; 285 } 286}; 287