arrow-body-style.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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('../types').Rule.RuleModule} */
  14. module.exports = {
  15. meta: {
  16. type: "suggestion",
  17. defaultOptions: ["as-needed"],
  18. docs: {
  19. description: "Require braces around arrow function bodies",
  20. recommended: false,
  21. frozen: true,
  22. url: "https://eslint.org/docs/latest/rules/arrow-body-style",
  23. },
  24. schema: {
  25. anyOf: [
  26. {
  27. type: "array",
  28. items: [
  29. {
  30. enum: ["always", "never"],
  31. },
  32. ],
  33. minItems: 0,
  34. maxItems: 1,
  35. },
  36. {
  37. type: "array",
  38. items: [
  39. {
  40. enum: ["as-needed"],
  41. },
  42. {
  43. type: "object",
  44. properties: {
  45. requireReturnForObjectLiteral: {
  46. type: "boolean",
  47. },
  48. },
  49. additionalProperties: false,
  50. },
  51. ],
  52. minItems: 0,
  53. maxItems: 2,
  54. },
  55. ],
  56. },
  57. fixable: "code",
  58. messages: {
  59. unexpectedOtherBlock:
  60. "Unexpected block statement surrounding arrow body.",
  61. unexpectedEmptyBlock:
  62. "Unexpected block statement surrounding arrow body; put a value of `undefined` immediately after the `=>`.",
  63. unexpectedObjectBlock:
  64. "Unexpected block statement surrounding arrow body; parenthesize the returned value and move it immediately after the `=>`.",
  65. unexpectedSingleBlock:
  66. "Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.",
  67. expectedBlock: "Expected block statement surrounding arrow body.",
  68. },
  69. },
  70. create(context) {
  71. const options = context.options;
  72. const always = options[0] === "always";
  73. const asNeeded = options[0] === "as-needed";
  74. const never = options[0] === "never";
  75. const requireReturnForObjectLiteral =
  76. options[1] && options[1].requireReturnForObjectLiteral;
  77. const sourceCode = context.sourceCode;
  78. let funcInfo = null;
  79. /**
  80. * Checks whether the given node has ASI problem or not.
  81. * @param {Token} token The token to check.
  82. * @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed.
  83. */
  84. function hasASIProblem(token) {
  85. return (
  86. token &&
  87. token.type === "Punctuator" &&
  88. /^[([/`+-]/u.test(token.value)
  89. );
  90. }
  91. /**
  92. * Gets the closing parenthesis by the given node.
  93. * @param {ASTNode} node first node after an opening parenthesis.
  94. * @returns {Token} The found closing parenthesis token.
  95. */
  96. function findClosingParen(node) {
  97. let nodeToCheck = node;
  98. while (!astUtils.isParenthesised(sourceCode, nodeToCheck)) {
  99. nodeToCheck = nodeToCheck.parent;
  100. }
  101. return sourceCode.getTokenAfter(nodeToCheck);
  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 (
  111. node.parent.type === "ForStatement" &&
  112. node.parent.init === node
  113. ) {
  114. return true;
  115. }
  116. return isInsideForLoopInitializer(node.parent);
  117. }
  118. return false;
  119. }
  120. /**
  121. * Determines whether a arrow function body needs braces
  122. * @param {ASTNode} node The arrow function node.
  123. * @returns {void}
  124. */
  125. function validate(node) {
  126. const arrowBody = node.body;
  127. if (arrowBody.type === "BlockStatement") {
  128. const blockBody = arrowBody.body;
  129. if (blockBody.length !== 1 && !never) {
  130. return;
  131. }
  132. if (
  133. asNeeded &&
  134. requireReturnForObjectLiteral &&
  135. blockBody[0].type === "ReturnStatement" &&
  136. blockBody[0].argument &&
  137. blockBody[0].argument.type === "ObjectExpression"
  138. ) {
  139. return;
  140. }
  141. if (
  142. never ||
  143. (asNeeded && blockBody[0].type === "ReturnStatement")
  144. ) {
  145. let messageId;
  146. if (blockBody.length === 0) {
  147. messageId = "unexpectedEmptyBlock";
  148. } else if (
  149. blockBody.length > 1 ||
  150. blockBody[0].type !== "ReturnStatement"
  151. ) {
  152. messageId = "unexpectedOtherBlock";
  153. } else if (blockBody[0].argument === null) {
  154. messageId = "unexpectedSingleBlock";
  155. } else if (
  156. astUtils.isOpeningBraceToken(
  157. sourceCode.getFirstToken(blockBody[0], { skip: 1 }),
  158. )
  159. ) {
  160. messageId = "unexpectedObjectBlock";
  161. } else {
  162. messageId = "unexpectedSingleBlock";
  163. }
  164. context.report({
  165. node,
  166. loc: arrowBody.loc,
  167. messageId,
  168. fix(fixer) {
  169. const fixes = [];
  170. if (
  171. blockBody.length !== 1 ||
  172. blockBody[0].type !== "ReturnStatement" ||
  173. !blockBody[0].argument ||
  174. hasASIProblem(
  175. sourceCode.getTokenAfter(arrowBody),
  176. )
  177. ) {
  178. return fixes;
  179. }
  180. const openingBrace =
  181. sourceCode.getFirstToken(arrowBody);
  182. const closingBrace =
  183. sourceCode.getLastToken(arrowBody);
  184. const firstValueToken = sourceCode.getFirstToken(
  185. blockBody[0],
  186. 1,
  187. );
  188. const lastValueToken = sourceCode.getLastToken(
  189. blockBody[0],
  190. );
  191. const commentsExist =
  192. sourceCode.commentsExistBetween(
  193. openingBrace,
  194. firstValueToken,
  195. ) ||
  196. sourceCode.commentsExistBetween(
  197. lastValueToken,
  198. closingBrace,
  199. );
  200. /*
  201. * Remove tokens around the return value.
  202. * If comments don't exist, remove extra spaces as well.
  203. */
  204. if (commentsExist) {
  205. fixes.push(
  206. fixer.remove(openingBrace),
  207. fixer.remove(closingBrace),
  208. fixer.remove(
  209. sourceCode.getTokenAfter(openingBrace),
  210. ), // return keyword
  211. );
  212. } else {
  213. fixes.push(
  214. fixer.removeRange([
  215. openingBrace.range[0],
  216. firstValueToken.range[0],
  217. ]),
  218. fixer.removeRange([
  219. lastValueToken.range[1],
  220. closingBrace.range[1],
  221. ]),
  222. );
  223. }
  224. /*
  225. * If the first token of the return value is `{` or the return value is a sequence expression,
  226. * enclose the return value by parentheses to avoid syntax error.
  227. */
  228. if (
  229. astUtils.isOpeningBraceToken(firstValueToken) ||
  230. blockBody[0].argument.type ===
  231. "SequenceExpression" ||
  232. (funcInfo.hasInOperator &&
  233. isInsideForLoopInitializer(node))
  234. ) {
  235. if (
  236. !astUtils.isParenthesised(
  237. sourceCode,
  238. blockBody[0].argument,
  239. )
  240. ) {
  241. fixes.push(
  242. fixer.insertTextBefore(
  243. firstValueToken,
  244. "(",
  245. ),
  246. fixer.insertTextAfter(
  247. lastValueToken,
  248. ")",
  249. ),
  250. );
  251. }
  252. }
  253. /*
  254. * If the last token of the return statement is semicolon, remove it.
  255. * Non-block arrow body is an expression, not a statement.
  256. */
  257. if (astUtils.isSemicolonToken(lastValueToken)) {
  258. fixes.push(fixer.remove(lastValueToken));
  259. }
  260. return fixes;
  261. },
  262. });
  263. }
  264. } else {
  265. if (
  266. always ||
  267. (asNeeded &&
  268. requireReturnForObjectLiteral &&
  269. arrowBody.type === "ObjectExpression")
  270. ) {
  271. context.report({
  272. node,
  273. loc: arrowBody.loc,
  274. messageId: "expectedBlock",
  275. fix(fixer) {
  276. const fixes = [];
  277. const arrowToken = sourceCode.getTokenBefore(
  278. arrowBody,
  279. astUtils.isArrowToken,
  280. );
  281. const [
  282. firstTokenAfterArrow,
  283. secondTokenAfterArrow,
  284. ] = sourceCode.getTokensAfter(arrowToken, {
  285. count: 2,
  286. });
  287. const lastToken = sourceCode.getLastToken(node);
  288. let parenthesisedObjectLiteral = null;
  289. if (
  290. astUtils.isOpeningParenToken(
  291. firstTokenAfterArrow,
  292. ) &&
  293. astUtils.isOpeningBraceToken(
  294. secondTokenAfterArrow,
  295. )
  296. ) {
  297. const braceNode =
  298. sourceCode.getNodeByRangeIndex(
  299. secondTokenAfterArrow.range[0],
  300. );
  301. if (braceNode.type === "ObjectExpression") {
  302. parenthesisedObjectLiteral = braceNode;
  303. }
  304. }
  305. // If the value is object literal, remove parentheses which were forced by syntax.
  306. if (parenthesisedObjectLiteral) {
  307. const openingParenToken = firstTokenAfterArrow;
  308. const openingBraceToken = secondTokenAfterArrow;
  309. if (
  310. astUtils.isTokenOnSameLine(
  311. openingParenToken,
  312. openingBraceToken,
  313. )
  314. ) {
  315. fixes.push(
  316. fixer.replaceText(
  317. openingParenToken,
  318. "{return ",
  319. ),
  320. );
  321. } else {
  322. // Avoid ASI
  323. fixes.push(
  324. fixer.replaceText(
  325. openingParenToken,
  326. "{",
  327. ),
  328. fixer.insertTextBefore(
  329. openingBraceToken,
  330. "return ",
  331. ),
  332. );
  333. }
  334. // Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo()
  335. fixes.push(
  336. fixer.remove(
  337. findClosingParen(
  338. parenthesisedObjectLiteral,
  339. ),
  340. ),
  341. );
  342. fixes.push(
  343. fixer.insertTextAfter(lastToken, "}"),
  344. );
  345. } else {
  346. fixes.push(
  347. fixer.insertTextBefore(
  348. firstTokenAfterArrow,
  349. "{return ",
  350. ),
  351. );
  352. fixes.push(
  353. fixer.insertTextAfter(lastToken, "}"),
  354. );
  355. }
  356. return fixes;
  357. },
  358. });
  359. }
  360. }
  361. }
  362. return {
  363. "BinaryExpression[operator='in']"() {
  364. let info = funcInfo;
  365. while (info) {
  366. info.hasInOperator = true;
  367. info = info.upper;
  368. }
  369. },
  370. ArrowFunctionExpression() {
  371. funcInfo = {
  372. upper: funcInfo,
  373. hasInOperator: false,
  374. };
  375. },
  376. "ArrowFunctionExpression:exit"(node) {
  377. validate(node);
  378. funcInfo = funcInfo.upper;
  379. },
  380. };
  381. },
  382. };