| /** |
| * @fileoverview Rule to require braces in arrow function body. |
| * @author Alberto RodrÃguez |
| */ |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const astUtils = require("./utils/ast-utils"); |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| /** @type {import('../shared/types').Rule} */ |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| docs: { |
| description: "require braces around arrow function bodies", |
| recommended: false, |
| url: "https://eslint.org/docs/rules/arrow-body-style" |
| }, |
| |
| schema: { |
| anyOf: [ |
| { |
| type: "array", |
| items: [ |
| { |
| enum: ["always", "never"] |
| } |
| ], |
| minItems: 0, |
| maxItems: 1 |
| }, |
| { |
| type: "array", |
| items: [ |
| { |
| enum: ["as-needed"] |
| }, |
| { |
| type: "object", |
| properties: { |
| requireReturnForObjectLiteral: { type: "boolean" } |
| }, |
| additionalProperties: false |
| } |
| ], |
| minItems: 0, |
| maxItems: 2 |
| } |
| ] |
| }, |
| |
| fixable: "code", |
| |
| messages: { |
| unexpectedOtherBlock: "Unexpected block statement surrounding arrow body.", |
| unexpectedEmptyBlock: "Unexpected block statement surrounding arrow body; put a value of `undefined` immediately after the `=>`.", |
| unexpectedObjectBlock: "Unexpected block statement surrounding arrow body; parenthesize the returned value and move it immediately after the `=>`.", |
| unexpectedSingleBlock: "Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.", |
| expectedBlock: "Expected block statement surrounding arrow body." |
| } |
| }, |
| |
| create(context) { |
| const options = context.options; |
| const always = options[0] === "always"; |
| const asNeeded = !options[0] || options[0] === "as-needed"; |
| const never = options[0] === "never"; |
| const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral; |
| const sourceCode = context.getSourceCode(); |
| let funcInfo = null; |
| |
| /** |
| * Checks whether the given node has ASI problem or not. |
| * @param {Token} token The token to check. |
| * @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed. |
| */ |
| function hasASIProblem(token) { |
| return token && token.type === "Punctuator" && /^[([/`+-]/u.test(token.value); |
| } |
| |
| /** |
| * Gets the closing parenthesis by the given node. |
| * @param {ASTNode} node first node after an opening parenthesis. |
| * @returns {Token} The found closing parenthesis token. |
| */ |
| function findClosingParen(node) { |
| let nodeToCheck = node; |
| |
| while (!astUtils.isParenthesised(sourceCode, nodeToCheck)) { |
| nodeToCheck = nodeToCheck.parent; |
| } |
| return sourceCode.getTokenAfter(nodeToCheck); |
| } |
| |
| /** |
| * Check whether the node is inside of a for loop's init |
| * @param {ASTNode} node node is inside for loop |
| * @returns {boolean} `true` if the node is inside of a for loop, else `false` |
| */ |
| function isInsideForLoopInitializer(node) { |
| if (node && node.parent) { |
| if (node.parent.type === "ForStatement" && node.parent.init === node) { |
| return true; |
| } |
| return isInsideForLoopInitializer(node.parent); |
| } |
| return false; |
| } |
| |
| /** |
| * Determines whether a arrow function body needs braces |
| * @param {ASTNode} node The arrow function node. |
| * @returns {void} |
| */ |
| function validate(node) { |
| const arrowBody = node.body; |
| |
| if (arrowBody.type === "BlockStatement") { |
| const blockBody = arrowBody.body; |
| |
| if (blockBody.length !== 1 && !never) { |
| return; |
| } |
| |
| if (asNeeded && requireReturnForObjectLiteral && blockBody[0].type === "ReturnStatement" && |
| blockBody[0].argument && blockBody[0].argument.type === "ObjectExpression") { |
| return; |
| } |
| |
| if (never || asNeeded && blockBody[0].type === "ReturnStatement") { |
| let messageId; |
| |
| if (blockBody.length === 0) { |
| messageId = "unexpectedEmptyBlock"; |
| } else if (blockBody.length > 1) { |
| messageId = "unexpectedOtherBlock"; |
| } else if (blockBody[0].argument === null) { |
| messageId = "unexpectedSingleBlock"; |
| } else if (astUtils.isOpeningBraceToken(sourceCode.getFirstToken(blockBody[0], { skip: 1 }))) { |
| messageId = "unexpectedObjectBlock"; |
| } else { |
| messageId = "unexpectedSingleBlock"; |
| } |
| |
| context.report({ |
| node, |
| loc: arrowBody.loc, |
| messageId, |
| fix(fixer) { |
| const fixes = []; |
| |
| if (blockBody.length !== 1 || |
| blockBody[0].type !== "ReturnStatement" || |
| !blockBody[0].argument || |
| hasASIProblem(sourceCode.getTokenAfter(arrowBody)) |
| ) { |
| return fixes; |
| } |
| |
| const openingBrace = sourceCode.getFirstToken(arrowBody); |
| const closingBrace = sourceCode.getLastToken(arrowBody); |
| const firstValueToken = sourceCode.getFirstToken(blockBody[0], 1); |
| const lastValueToken = sourceCode.getLastToken(blockBody[0]); |
| const commentsExist = |
| sourceCode.commentsExistBetween(openingBrace, firstValueToken) || |
| sourceCode.commentsExistBetween(lastValueToken, closingBrace); |
| |
| /* |
| * Remove tokens around the return value. |
| * If comments don't exist, remove extra spaces as well. |
| */ |
| if (commentsExist) { |
| fixes.push( |
| fixer.remove(openingBrace), |
| fixer.remove(closingBrace), |
| fixer.remove(sourceCode.getTokenAfter(openingBrace)) // return keyword |
| ); |
| } else { |
| fixes.push( |
| fixer.removeRange([openingBrace.range[0], firstValueToken.range[0]]), |
| fixer.removeRange([lastValueToken.range[1], closingBrace.range[1]]) |
| ); |
| } |
| |
| /* |
| * If the first token of the return value is `{` or the return value is a sequence expression, |
| * enclose the return value by parentheses to avoid syntax error. |
| */ |
| if (astUtils.isOpeningBraceToken(firstValueToken) || blockBody[0].argument.type === "SequenceExpression" || (funcInfo.hasInOperator && isInsideForLoopInitializer(node))) { |
| if (!astUtils.isParenthesised(sourceCode, blockBody[0].argument)) { |
| fixes.push( |
| fixer.insertTextBefore(firstValueToken, "("), |
| fixer.insertTextAfter(lastValueToken, ")") |
| ); |
| } |
| } |
| |
| /* |
| * If the last token of the return statement is semicolon, remove it. |
| * Non-block arrow body is an expression, not a statement. |
| */ |
| if (astUtils.isSemicolonToken(lastValueToken)) { |
| fixes.push(fixer.remove(lastValueToken)); |
| } |
| |
| return fixes; |
| } |
| }); |
| } |
| } else { |
| if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) { |
| context.report({ |
| node, |
| loc: arrowBody.loc, |
| messageId: "expectedBlock", |
| fix(fixer) { |
| const fixes = []; |
| const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken); |
| const [firstTokenAfterArrow, secondTokenAfterArrow] = sourceCode.getTokensAfter(arrowToken, { count: 2 }); |
| const lastToken = sourceCode.getLastToken(node); |
| |
| let parenthesisedObjectLiteral = null; |
| |
| if ( |
| astUtils.isOpeningParenToken(firstTokenAfterArrow) && |
| astUtils.isOpeningBraceToken(secondTokenAfterArrow) |
| ) { |
| const braceNode = sourceCode.getNodeByRangeIndex(secondTokenAfterArrow.range[0]); |
| |
| if (braceNode.type === "ObjectExpression") { |
| parenthesisedObjectLiteral = braceNode; |
| } |
| } |
| |
| // If the value is object literal, remove parentheses which were forced by syntax. |
| if (parenthesisedObjectLiteral) { |
| const openingParenToken = firstTokenAfterArrow; |
| const openingBraceToken = secondTokenAfterArrow; |
| |
| if (astUtils.isTokenOnSameLine(openingParenToken, openingBraceToken)) { |
| fixes.push(fixer.replaceText(openingParenToken, "{return ")); |
| } else { |
| |
| // Avoid ASI |
| fixes.push( |
| fixer.replaceText(openingParenToken, "{"), |
| fixer.insertTextBefore(openingBraceToken, "return ") |
| ); |
| } |
| |
| // Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo() |
| fixes.push(fixer.remove(findClosingParen(parenthesisedObjectLiteral))); |
| fixes.push(fixer.insertTextAfter(lastToken, "}")); |
| |
| } else { |
| fixes.push(fixer.insertTextBefore(firstTokenAfterArrow, "{return ")); |
| fixes.push(fixer.insertTextAfter(lastToken, "}")); |
| } |
| |
| return fixes; |
| } |
| }); |
| } |
| } |
| } |
| |
| return { |
| "BinaryExpression[operator='in']"() { |
| let info = funcInfo; |
| |
| while (info) { |
| info.hasInOperator = true; |
| info = info.upper; |
| } |
| }, |
| ArrowFunctionExpression() { |
| funcInfo = { |
| upper: funcInfo, |
| hasInOperator: false |
| }; |
| }, |
| "ArrowFunctionExpression:exit"(node) { |
| validate(node); |
| funcInfo = funcInfo.upper; |
| } |
| }; |
| } |
| }; |