1 | /**
|
2 | * @fileoverview Rule to require braces in arrow function body.
|
3 | * @author Alberto Rodríguez
|
4 | */
|
5 | ;
|
6 |
|
7 | //------------------------------------------------------------------------------
|
8 | // Requirements
|
9 | //------------------------------------------------------------------------------
|
10 |
|
11 | const astUtils = require("../ast-utils");
|
12 |
|
13 | //------------------------------------------------------------------------------
|
14 | // Rule Definition
|
15 | //------------------------------------------------------------------------------
|
16 |
|
17 | module.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 | };
|