• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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