[d565449] | 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 |
|
---|
| 11 | const astUtils = require("./utils/ast-utils");
|
---|
| 12 |
|
---|
| 13 | //------------------------------------------------------------------------------
|
---|
| 14 | // Rule Definition
|
---|
| 15 | //------------------------------------------------------------------------------
|
---|
| 16 |
|
---|
| 17 | /** @type {import('../shared/types').Rule} */
|
---|
| 18 | module.exports = {
|
---|
| 19 | meta: {
|
---|
| 20 | type: "suggestion",
|
---|
| 21 |
|
---|
| 22 | docs: {
|
---|
| 23 | description: "Require braces around arrow function bodies",
|
---|
| 24 | recommended: false,
|
---|
| 25 | url: "https://eslint.org/docs/latest/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.sourceCode;
|
---|
| 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 by the given node.
|
---|
| 91 | * @param {ASTNode} node first node after an opening parenthesis.
|
---|
| 92 | * @returns {Token} The found closing parenthesis token.
|
---|
| 93 | */
|
---|
| 94 | function findClosingParen(node) {
|
---|
| 95 | let nodeToCheck = node;
|
---|
| 96 |
|
---|
| 97 | while (!astUtils.isParenthesised(sourceCode, nodeToCheck)) {
|
---|
| 98 | nodeToCheck = nodeToCheck.parent;
|
---|
| 99 | }
|
---|
| 100 | return sourceCode.getTokenAfter(nodeToCheck);
|
---|
| 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,
|
---|
| 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 return 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,
|
---|
| 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 |
|
---|
| 230 | let parenthesisedObjectLiteral = null;
|
---|
| 231 |
|
---|
| 232 | if (
|
---|
| 233 | astUtils.isOpeningParenToken(firstTokenAfterArrow) &&
|
---|
| 234 | astUtils.isOpeningBraceToken(secondTokenAfterArrow)
|
---|
| 235 | ) {
|
---|
| 236 | const braceNode = sourceCode.getNodeByRangeIndex(secondTokenAfterArrow.range[0]);
|
---|
| 237 |
|
---|
| 238 | if (braceNode.type === "ObjectExpression") {
|
---|
| 239 | parenthesisedObjectLiteral = braceNode;
|
---|
| 240 | }
|
---|
| 241 | }
|
---|
| 242 |
|
---|
| 243 | // If the value is object literal, remove parentheses which were forced by syntax.
|
---|
| 244 | if (parenthesisedObjectLiteral) {
|
---|
| 245 | const openingParenToken = firstTokenAfterArrow;
|
---|
| 246 | const openingBraceToken = secondTokenAfterArrow;
|
---|
| 247 |
|
---|
| 248 | if (astUtils.isTokenOnSameLine(openingParenToken, openingBraceToken)) {
|
---|
| 249 | fixes.push(fixer.replaceText(openingParenToken, "{return "));
|
---|
| 250 | } else {
|
---|
| 251 |
|
---|
| 252 | // Avoid ASI
|
---|
| 253 | fixes.push(
|
---|
| 254 | fixer.replaceText(openingParenToken, "{"),
|
---|
| 255 | fixer.insertTextBefore(openingBraceToken, "return ")
|
---|
| 256 | );
|
---|
| 257 | }
|
---|
| 258 |
|
---|
| 259 | // Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo()
|
---|
| 260 | fixes.push(fixer.remove(findClosingParen(parenthesisedObjectLiteral)));
|
---|
| 261 | fixes.push(fixer.insertTextAfter(lastToken, "}"));
|
---|
| 262 |
|
---|
| 263 | } else {
|
---|
| 264 | fixes.push(fixer.insertTextBefore(firstTokenAfterArrow, "{return "));
|
---|
| 265 | fixes.push(fixer.insertTextAfter(lastToken, "}"));
|
---|
| 266 | }
|
---|
| 267 |
|
---|
| 268 | return fixes;
|
---|
| 269 | }
|
---|
| 270 | });
|
---|
| 271 | }
|
---|
| 272 | }
|
---|
| 273 | }
|
---|
| 274 |
|
---|
| 275 | return {
|
---|
| 276 | "BinaryExpression[operator='in']"() {
|
---|
| 277 | let info = funcInfo;
|
---|
| 278 |
|
---|
| 279 | while (info) {
|
---|
| 280 | info.hasInOperator = true;
|
---|
| 281 | info = info.upper;
|
---|
| 282 | }
|
---|
| 283 | },
|
---|
| 284 | ArrowFunctionExpression() {
|
---|
| 285 | funcInfo = {
|
---|
| 286 | upper: funcInfo,
|
---|
| 287 | hasInOperator: false
|
---|
| 288 | };
|
---|
| 289 | },
|
---|
| 290 | "ArrowFunctionExpression:exit"(node) {
|
---|
| 291 | validate(node);
|
---|
| 292 | funcInfo = funcInfo.upper;
|
---|
| 293 | }
|
---|
| 294 | };
|
---|
| 295 | }
|
---|
| 296 | };
|
---|