1/** 2 * @fileoverview Rule to flag statements without curly braces 3 * @author Nicholas C. Zakas 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: "enforce consistent brace style for all control statements", 23 category: "Best Practices", 24 recommended: false, 25 url: "https://eslint.org/docs/rules/curly" 26 }, 27 28 schema: { 29 anyOf: [ 30 { 31 type: "array", 32 items: [ 33 { 34 enum: ["all"] 35 } 36 ], 37 minItems: 0, 38 maxItems: 1 39 }, 40 { 41 type: "array", 42 items: [ 43 { 44 enum: ["multi", "multi-line", "multi-or-nest"] 45 }, 46 { 47 enum: ["consistent"] 48 } 49 ], 50 minItems: 0, 51 maxItems: 2 52 } 53 ] 54 }, 55 56 fixable: "code", 57 58 messages: { 59 missingCurlyAfter: "Expected { after '{{name}}'.", 60 missingCurlyAfterCondition: "Expected { after '{{name}}' condition.", 61 unexpectedCurlyAfter: "Unnecessary { after '{{name}}'.", 62 unexpectedCurlyAfterCondition: "Unnecessary { after '{{name}}' condition." 63 } 64 }, 65 66 create(context) { 67 68 const multiOnly = (context.options[0] === "multi"); 69 const multiLine = (context.options[0] === "multi-line"); 70 const multiOrNest = (context.options[0] === "multi-or-nest"); 71 const consistent = (context.options[1] === "consistent"); 72 73 const sourceCode = context.getSourceCode(); 74 75 //-------------------------------------------------------------------------- 76 // Helpers 77 //-------------------------------------------------------------------------- 78 79 /** 80 * Determines if a given node is a one-liner that's on the same line as it's preceding code. 81 * @param {ASTNode} node The node to check. 82 * @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code. 83 * @private 84 */ 85 function isCollapsedOneLiner(node) { 86 const before = sourceCode.getTokenBefore(node); 87 const last = sourceCode.getLastToken(node); 88 const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last; 89 90 return before.loc.start.line === lastExcludingSemicolon.loc.end.line; 91 } 92 93 /** 94 * Determines if a given node is a one-liner. 95 * @param {ASTNode} node The node to check. 96 * @returns {boolean} True if the node is a one-liner. 97 * @private 98 */ 99 function isOneLiner(node) { 100 if (node.type === "EmptyStatement") { 101 return true; 102 } 103 104 const first = sourceCode.getFirstToken(node); 105 const last = sourceCode.getLastToken(node); 106 const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last; 107 108 return first.loc.start.line === lastExcludingSemicolon.loc.end.line; 109 } 110 111 /** 112 * Determines if the given node is a lexical declaration (let, const, function, or class) 113 * @param {ASTNode} node The node to check 114 * @returns {boolean} True if the node is a lexical declaration 115 * @private 116 */ 117 function isLexicalDeclaration(node) { 118 if (node.type === "VariableDeclaration") { 119 return node.kind === "const" || node.kind === "let"; 120 } 121 122 return node.type === "FunctionDeclaration" || node.type === "ClassDeclaration"; 123 } 124 125 /** 126 * Checks if the given token is an `else` token or not. 127 * @param {Token} token The token to check. 128 * @returns {boolean} `true` if the token is an `else` token. 129 */ 130 function isElseKeywordToken(token) { 131 return token.value === "else" && token.type === "Keyword"; 132 } 133 134 /** 135 * Gets the `else` keyword token of a given `IfStatement` node. 136 * @param {ASTNode} node A `IfStatement` node to get. 137 * @returns {Token} The `else` keyword token. 138 */ 139 function getElseKeyword(node) { 140 return node.alternate && sourceCode.getFirstTokenBetween(node.consequent, node.alternate, isElseKeywordToken); 141 } 142 143 /** 144 * Determines whether the given node has an `else` keyword token as the first token after. 145 * @param {ASTNode} node The node to check. 146 * @returns {boolean} `true` if the node is followed by an `else` keyword token. 147 */ 148 function isFollowedByElseKeyword(node) { 149 const nextToken = sourceCode.getTokenAfter(node); 150 151 return Boolean(nextToken) && isElseKeywordToken(nextToken); 152 } 153 154 /** 155 * Determines if a semicolon needs to be inserted after removing a set of curly brackets, in order to avoid a SyntaxError. 156 * @param {Token} closingBracket The } token 157 * @returns {boolean} `true` if a semicolon needs to be inserted after the last statement in the block. 158 */ 159 function needsSemicolon(closingBracket) { 160 const tokenBefore = sourceCode.getTokenBefore(closingBracket); 161 const tokenAfter = sourceCode.getTokenAfter(closingBracket); 162 const lastBlockNode = sourceCode.getNodeByRangeIndex(tokenBefore.range[0]); 163 164 if (astUtils.isSemicolonToken(tokenBefore)) { 165 166 // If the last statement already has a semicolon, don't add another one. 167 return false; 168 } 169 170 if (!tokenAfter) { 171 172 // If there are no statements after this block, there is no need to add a semicolon. 173 return false; 174 } 175 176 if (lastBlockNode.type === "BlockStatement" && lastBlockNode.parent.type !== "FunctionExpression" && lastBlockNode.parent.type !== "ArrowFunctionExpression") { 177 178 /* 179 * If the last node surrounded by curly brackets is a BlockStatement (other than a FunctionExpression or an ArrowFunctionExpression), 180 * don't insert a semicolon. Otherwise, the semicolon would be parsed as a separate statement, which would cause 181 * a SyntaxError if it was followed by `else`. 182 */ 183 return false; 184 } 185 186 if (tokenBefore.loc.end.line === tokenAfter.loc.start.line) { 187 188 // If the next token is on the same line, insert a semicolon. 189 return true; 190 } 191 192 if (/^[([/`+-]/u.test(tokenAfter.value)) { 193 194 // If the next token starts with a character that would disrupt ASI, insert a semicolon. 195 return true; 196 } 197 198 if (tokenBefore.type === "Punctuator" && (tokenBefore.value === "++" || tokenBefore.value === "--")) { 199 200 // If the last token is ++ or --, insert a semicolon to avoid disrupting ASI. 201 return true; 202 } 203 204 // Otherwise, do not insert a semicolon. 205 return false; 206 } 207 208 /** 209 * Determines whether the code represented by the given node contains an `if` statement 210 * that would become associated with an `else` keyword directly appended to that code. 211 * 212 * Examples where it returns `true`: 213 * 214 * if (a) 215 * foo(); 216 * 217 * if (a) { 218 * foo(); 219 * } 220 * 221 * if (a) 222 * foo(); 223 * else if (b) 224 * bar(); 225 * 226 * while (a) 227 * if (b) 228 * if(c) 229 * foo(); 230 * else 231 * bar(); 232 * 233 * Examples where it returns `false`: 234 * 235 * if (a) 236 * foo(); 237 * else 238 * bar(); 239 * 240 * while (a) { 241 * if (b) 242 * if(c) 243 * foo(); 244 * else 245 * bar(); 246 * } 247 * 248 * while (a) 249 * if (b) { 250 * if(c) 251 * foo(); 252 * } 253 * else 254 * bar(); 255 * @param {ASTNode} node Node representing the code to check. 256 * @returns {boolean} `true` if an `if` statement within the code would become associated with an `else` appended to that code. 257 */ 258 function hasUnsafeIf(node) { 259 switch (node.type) { 260 case "IfStatement": 261 if (!node.alternate) { 262 return true; 263 } 264 return hasUnsafeIf(node.alternate); 265 case "ForStatement": 266 case "ForInStatement": 267 case "ForOfStatement": 268 case "LabeledStatement": 269 case "WithStatement": 270 case "WhileStatement": 271 return hasUnsafeIf(node.body); 272 default: 273 return false; 274 } 275 } 276 277 /** 278 * Determines whether the existing curly braces around the single statement are necessary to preserve the semantics of the code. 279 * The braces, which make the given block body, are necessary in either of the following situations: 280 * 281 * 1. The statement is a lexical declaration. 282 * 2. Without the braces, an `if` within the statement would become associated with an `else` after the closing brace: 283 * 284 * if (a) { 285 * if (b) 286 * foo(); 287 * } 288 * else 289 * bar(); 290 * 291 * if (a) 292 * while (b) 293 * while (c) { 294 * while (d) 295 * if (e) 296 * while(f) 297 * foo(); 298 * } 299 * else 300 * bar(); 301 * @param {ASTNode} node `BlockStatement` body with exactly one statement directly inside. The statement can have its own nested statements. 302 * @returns {boolean} `true` if the braces are necessary - removing them (replacing the given `BlockStatement` body with its single statement content) 303 * would change the semantics of the code or produce a syntax error. 304 */ 305 function areBracesNecessary(node) { 306 const statement = node.body[0]; 307 308 return isLexicalDeclaration(statement) || 309 hasUnsafeIf(statement) && isFollowedByElseKeyword(node); 310 } 311 312 /** 313 * Prepares to check the body of a node to see if it's a block statement. 314 * @param {ASTNode} node The node to report if there's a problem. 315 * @param {ASTNode} body The body node to check for blocks. 316 * @param {string} name The name to report if there's a problem. 317 * @param {{ condition: boolean }} opts Options to pass to the report functions 318 * @returns {Object} a prepared check object, with "actual", "expected", "check" properties. 319 * "actual" will be `true` or `false` whether the body is already a block statement. 320 * "expected" will be `true` or `false` if the body should be a block statement or not, or 321 * `null` if it doesn't matter, depending on the rule options. It can be modified to change 322 * the final behavior of "check". 323 * "check" will be a function reporting appropriate problems depending on the other 324 * properties. 325 */ 326 function prepareCheck(node, body, name, opts) { 327 const hasBlock = (body.type === "BlockStatement"); 328 let expected = null; 329 330 if (hasBlock && (body.body.length !== 1 || areBracesNecessary(body))) { 331 expected = true; 332 } else if (multiOnly) { 333 expected = false; 334 } else if (multiLine) { 335 if (!isCollapsedOneLiner(body)) { 336 expected = true; 337 } 338 339 // otherwise, the body is allowed to have braces or not to have braces 340 341 } else if (multiOrNest) { 342 if (hasBlock) { 343 const statement = body.body[0]; 344 const leadingCommentsInBlock = sourceCode.getCommentsBefore(statement); 345 346 expected = !isOneLiner(statement) || leadingCommentsInBlock.length > 0; 347 } else { 348 expected = !isOneLiner(body); 349 } 350 } else { 351 352 // default "all" 353 expected = true; 354 } 355 356 return { 357 actual: hasBlock, 358 expected, 359 check() { 360 if (this.expected !== null && this.expected !== this.actual) { 361 if (this.expected) { 362 context.report({ 363 node, 364 loc: (name !== "else" ? node : getElseKeyword(node)).loc.start, 365 messageId: opts && opts.condition ? "missingCurlyAfterCondition" : "missingCurlyAfter", 366 data: { 367 name 368 }, 369 fix: fixer => fixer.replaceText(body, `{${sourceCode.getText(body)}}`) 370 }); 371 } else { 372 context.report({ 373 node, 374 loc: (name !== "else" ? node : getElseKeyword(node)).loc.start, 375 messageId: opts && opts.condition ? "unexpectedCurlyAfterCondition" : "unexpectedCurlyAfter", 376 data: { 377 name 378 }, 379 fix(fixer) { 380 381 /* 382 * `do while` expressions sometimes need a space to be inserted after `do`. 383 * e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)` 384 */ 385 const needsPrecedingSpace = node.type === "DoWhileStatement" && 386 sourceCode.getTokenBefore(body).range[1] === body.range[0] && 387 !astUtils.canTokensBeAdjacent("do", sourceCode.getFirstToken(body, { skip: 1 })); 388 389 const openingBracket = sourceCode.getFirstToken(body); 390 const closingBracket = sourceCode.getLastToken(body); 391 const lastTokenInBlock = sourceCode.getTokenBefore(closingBracket); 392 393 if (needsSemicolon(closingBracket)) { 394 395 /* 396 * If removing braces would cause a SyntaxError due to multiple statements on the same line (or 397 * change the semantics of the code due to ASI), don't perform a fix. 398 */ 399 return null; 400 } 401 402 const resultingBodyText = sourceCode.getText().slice(openingBracket.range[1], lastTokenInBlock.range[0]) + 403 sourceCode.getText(lastTokenInBlock) + 404 sourceCode.getText().slice(lastTokenInBlock.range[1], closingBracket.range[0]); 405 406 return fixer.replaceText(body, (needsPrecedingSpace ? " " : "") + resultingBodyText); 407 } 408 }); 409 } 410 } 411 } 412 }; 413 } 414 415 /** 416 * Prepares to check the bodies of a "if", "else if" and "else" chain. 417 * @param {ASTNode} node The first IfStatement node of the chain. 418 * @returns {Object[]} prepared checks for each body of the chain. See `prepareCheck` for more 419 * information. 420 */ 421 function prepareIfChecks(node) { 422 const preparedChecks = []; 423 424 for (let currentNode = node; currentNode; currentNode = currentNode.alternate) { 425 preparedChecks.push(prepareCheck(currentNode, currentNode.consequent, "if", { condition: true })); 426 if (currentNode.alternate && currentNode.alternate.type !== "IfStatement") { 427 preparedChecks.push(prepareCheck(currentNode, currentNode.alternate, "else")); 428 break; 429 } 430 } 431 432 if (consistent) { 433 434 /* 435 * If any node should have or already have braces, make sure they 436 * all have braces. 437 * If all nodes shouldn't have braces, make sure they don't. 438 */ 439 const expected = preparedChecks.some(preparedCheck => { 440 if (preparedCheck.expected !== null) { 441 return preparedCheck.expected; 442 } 443 return preparedCheck.actual; 444 }); 445 446 preparedChecks.forEach(preparedCheck => { 447 preparedCheck.expected = expected; 448 }); 449 } 450 451 return preparedChecks; 452 } 453 454 //-------------------------------------------------------------------------- 455 // Public 456 //-------------------------------------------------------------------------- 457 458 return { 459 IfStatement(node) { 460 if (node.parent.type !== "IfStatement") { 461 prepareIfChecks(node).forEach(preparedCheck => { 462 preparedCheck.check(); 463 }); 464 } 465 }, 466 467 WhileStatement(node) { 468 prepareCheck(node, node.body, "while", { condition: true }).check(); 469 }, 470 471 DoWhileStatement(node) { 472 prepareCheck(node, node.body, "do").check(); 473 }, 474 475 ForStatement(node) { 476 prepareCheck(node, node.body, "for", { condition: true }).check(); 477 }, 478 479 ForInStatement(node) { 480 prepareCheck(node, node.body, "for-in").check(); 481 }, 482 483 ForOfStatement(node) { 484 prepareCheck(node, node.body, "for-of").check(); 485 } 486 }; 487 } 488}; 489