UNPKG

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