/** * @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('../types').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', defaultOptions: ['as-needed'], docs: { description: 'Require braces around arrow function bodies', recommended: false, frozen: true, url: 'https://eslint.org/docs/latest/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] === 'as-needed'; const never = options[0] === 'never'; const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral; const sourceCode = context.sourceCode; 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 || blockBody[0].type !== 'ReturnStatement' ) { 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; }, }; }, };