arrow-body-style.js 13 KB

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