--- /dev/null
+/**
+ * @fileoverview Rule to flag statements without curly braces
+ * @author Nicholas C. Zakas
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const astUtils = require("./utils/ast-utils");
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: "suggestion",
+
+ docs: {
+ description: "enforce consistent brace style for all control statements",
+ category: "Best Practices",
+ recommended: false,
+ url: "https://eslint.org/docs/rules/curly"
+ },
+
+ schema: {
+ anyOf: [
+ {
+ type: "array",
+ items: [
+ {
+ enum: ["all"]
+ }
+ ],
+ minItems: 0,
+ maxItems: 1
+ },
+ {
+ type: "array",
+ items: [
+ {
+ enum: ["multi", "multi-line", "multi-or-nest"]
+ },
+ {
+ enum: ["consistent"]
+ }
+ ],
+ minItems: 0,
+ maxItems: 2
+ }
+ ]
+ },
+
+ fixable: "code",
+
+ messages: {
+ missingCurlyAfter: "Expected { after '{{name}}'.",
+ missingCurlyAfterCondition: "Expected { after '{{name}}' condition.",
+ unexpectedCurlyAfter: "Unnecessary { after '{{name}}'.",
+ unexpectedCurlyAfterCondition: "Unnecessary { after '{{name}}' condition."
+ }
+ },
+
+ create(context) {
+
+ const multiOnly = (context.options[0] === "multi");
+ const multiLine = (context.options[0] === "multi-line");
+ const multiOrNest = (context.options[0] === "multi-or-nest");
+ const consistent = (context.options[1] === "consistent");
+
+ const sourceCode = context.getSourceCode();
+
+ //--------------------------------------------------------------------------
+ // Helpers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Determines if a given node is a one-liner that's on the same line as it's preceding code.
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code.
+ * @private
+ */
+ function isCollapsedOneLiner(node) {
+ const before = sourceCode.getTokenBefore(node);
+ const last = sourceCode.getLastToken(node);
+ const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last;
+
+ return before.loc.start.line === lastExcludingSemicolon.loc.end.line;
+ }
+
+ /**
+ * Determines if a given node is a one-liner.
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} True if the node is a one-liner.
+ * @private
+ */
+ function isOneLiner(node) {
+ if (node.type === "EmptyStatement") {
+ return true;
+ }
+
+ const first = sourceCode.getFirstToken(node);
+ const last = sourceCode.getLastToken(node);
+ const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last;
+
+ return first.loc.start.line === lastExcludingSemicolon.loc.end.line;
+ }
+
+ /**
+ * Determines if the given node is a lexical declaration (let, const, function, or class)
+ * @param {ASTNode} node The node to check
+ * @returns {boolean} True if the node is a lexical declaration
+ * @private
+ */
+ function isLexicalDeclaration(node) {
+ if (node.type === "VariableDeclaration") {
+ return node.kind === "const" || node.kind === "let";
+ }
+
+ return node.type === "FunctionDeclaration" || node.type === "ClassDeclaration";
+ }
+
+ /**
+ * Checks if the given token is an `else` token or not.
+ * @param {Token} token The token to check.
+ * @returns {boolean} `true` if the token is an `else` token.
+ */
+ function isElseKeywordToken(token) {
+ return token.value === "else" && token.type === "Keyword";
+ }
+
+ /**
+ * Gets the `else` keyword token of a given `IfStatement` node.
+ * @param {ASTNode} node A `IfStatement` node to get.
+ * @returns {Token} The `else` keyword token.
+ */
+ function getElseKeyword(node) {
+ return node.alternate && sourceCode.getFirstTokenBetween(node.consequent, node.alternate, isElseKeywordToken);
+ }
+
+ /**
+ * Determines whether the given node has an `else` keyword token as the first token after.
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} `true` if the node is followed by an `else` keyword token.
+ */
+ function isFollowedByElseKeyword(node) {
+ const nextToken = sourceCode.getTokenAfter(node);
+
+ return Boolean(nextToken) && isElseKeywordToken(nextToken);
+ }
+
+ /**
+ * Determines if a semicolon needs to be inserted after removing a set of curly brackets, in order to avoid a SyntaxError.
+ * @param {Token} closingBracket The } token
+ * @returns {boolean} `true` if a semicolon needs to be inserted after the last statement in the block.
+ */
+ function needsSemicolon(closingBracket) {
+ const tokenBefore = sourceCode.getTokenBefore(closingBracket);
+ const tokenAfter = sourceCode.getTokenAfter(closingBracket);
+ const lastBlockNode = sourceCode.getNodeByRangeIndex(tokenBefore.range[0]);
+
+ if (astUtils.isSemicolonToken(tokenBefore)) {
+
+ // If the last statement already has a semicolon, don't add another one.
+ return false;
+ }
+
+ if (!tokenAfter) {
+
+ // If there are no statements after this block, there is no need to add a semicolon.
+ return false;
+ }
+
+ if (lastBlockNode.type === "BlockStatement" && lastBlockNode.parent.type !== "FunctionExpression" && lastBlockNode.parent.type !== "ArrowFunctionExpression") {
+
+ /*
+ * If the last node surrounded by curly brackets is a BlockStatement (other than a FunctionExpression or an ArrowFunctionExpression),
+ * don't insert a semicolon. Otherwise, the semicolon would be parsed as a separate statement, which would cause
+ * a SyntaxError if it was followed by `else`.
+ */
+ return false;
+ }
+
+ if (tokenBefore.loc.end.line === tokenAfter.loc.start.line) {
+
+ // If the next token is on the same line, insert a semicolon.
+ return true;
+ }
+
+ if (/^[([/`+-]/u.test(tokenAfter.value)) {
+
+ // If the next token starts with a character that would disrupt ASI, insert a semicolon.
+ return true;
+ }
+
+ if (tokenBefore.type === "Punctuator" && (tokenBefore.value === "++" || tokenBefore.value === "--")) {
+
+ // If the last token is ++ or --, insert a semicolon to avoid disrupting ASI.
+ return true;
+ }
+
+ // Otherwise, do not insert a semicolon.
+ return false;
+ }
+
+ /**
+ * Determines whether the code represented by the given node contains an `if` statement
+ * that would become associated with an `else` keyword directly appended to that code.
+ *
+ * Examples where it returns `true`:
+ *
+ * if (a)
+ * foo();
+ *
+ * if (a) {
+ * foo();
+ * }
+ *
+ * if (a)
+ * foo();
+ * else if (b)
+ * bar();
+ *
+ * while (a)
+ * if (b)
+ * if(c)
+ * foo();
+ * else
+ * bar();
+ *
+ * Examples where it returns `false`:
+ *
+ * if (a)
+ * foo();
+ * else
+ * bar();
+ *
+ * while (a) {
+ * if (b)
+ * if(c)
+ * foo();
+ * else
+ * bar();
+ * }
+ *
+ * while (a)
+ * if (b) {
+ * if(c)
+ * foo();
+ * }
+ * else
+ * bar();
+ * @param {ASTNode} node Node representing the code to check.
+ * @returns {boolean} `true` if an `if` statement within the code would become associated with an `else` appended to that code.
+ */
+ function hasUnsafeIf(node) {
+ switch (node.type) {
+ case "IfStatement":
+ if (!node.alternate) {
+ return true;
+ }
+ return hasUnsafeIf(node.alternate);
+ case "ForStatement":
+ case "ForInStatement":
+ case "ForOfStatement":
+ case "LabeledStatement":
+ case "WithStatement":
+ case "WhileStatement":
+ return hasUnsafeIf(node.body);
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Determines whether the existing curly braces around the single statement are necessary to preserve the semantics of the code.
+ * The braces, which make the given block body, are necessary in either of the following situations:
+ *
+ * 1. The statement is a lexical declaration.
+ * 2. Without the braces, an `if` within the statement would become associated with an `else` after the closing brace:
+ *
+ * if (a) {
+ * if (b)
+ * foo();
+ * }
+ * else
+ * bar();
+ *
+ * if (a)
+ * while (b)
+ * while (c) {
+ * while (d)
+ * if (e)
+ * while(f)
+ * foo();
+ * }
+ * else
+ * bar();
+ * @param {ASTNode} node `BlockStatement` body with exactly one statement directly inside. The statement can have its own nested statements.
+ * @returns {boolean} `true` if the braces are necessary - removing them (replacing the given `BlockStatement` body with its single statement content)
+ * would change the semantics of the code or produce a syntax error.
+ */
+ function areBracesNecessary(node) {
+ const statement = node.body[0];
+
+ return isLexicalDeclaration(statement) ||
+ hasUnsafeIf(statement) && isFollowedByElseKeyword(node);
+ }
+
+ /**
+ * Prepares to check the body of a node to see if it's a block statement.
+ * @param {ASTNode} node The node to report if there's a problem.
+ * @param {ASTNode} body The body node to check for blocks.
+ * @param {string} name The name to report if there's a problem.
+ * @param {{ condition: boolean }} opts Options to pass to the report functions
+ * @returns {Object} a prepared check object, with "actual", "expected", "check" properties.
+ * "actual" will be `true` or `false` whether the body is already a block statement.
+ * "expected" will be `true` or `false` if the body should be a block statement or not, or
+ * `null` if it doesn't matter, depending on the rule options. It can be modified to change
+ * the final behavior of "check".
+ * "check" will be a function reporting appropriate problems depending on the other
+ * properties.
+ */
+ function prepareCheck(node, body, name, opts) {
+ const hasBlock = (body.type === "BlockStatement");
+ let expected = null;
+
+ if (hasBlock && (body.body.length !== 1 || areBracesNecessary(body))) {
+ expected = true;
+ } else if (multiOnly) {
+ expected = false;
+ } else if (multiLine) {
+ if (!isCollapsedOneLiner(body)) {
+ expected = true;
+ }
+
+ // otherwise, the body is allowed to have braces or not to have braces
+
+ } else if (multiOrNest) {
+ if (hasBlock) {
+ const statement = body.body[0];
+ const leadingCommentsInBlock = sourceCode.getCommentsBefore(statement);
+
+ expected = !isOneLiner(statement) || leadingCommentsInBlock.length > 0;
+ } else {
+ expected = !isOneLiner(body);
+ }
+ } else {
+
+ // default "all"
+ expected = true;
+ }
+
+ return {
+ actual: hasBlock,
+ expected,
+ check() {
+ if (this.expected !== null && this.expected !== this.actual) {
+ if (this.expected) {
+ context.report({
+ node,
+ loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
+ messageId: opts && opts.condition ? "missingCurlyAfterCondition" : "missingCurlyAfter",
+ data: {
+ name
+ },
+ fix: fixer => fixer.replaceText(body, `{${sourceCode.getText(body)}}`)
+ });
+ } else {
+ context.report({
+ node,
+ loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
+ messageId: opts && opts.condition ? "unexpectedCurlyAfterCondition" : "unexpectedCurlyAfter",
+ data: {
+ name
+ },
+ fix(fixer) {
+
+ /*
+ * `do while` expressions sometimes need a space to be inserted after `do`.
+ * e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)`
+ */
+ const needsPrecedingSpace = node.type === "DoWhileStatement" &&
+ sourceCode.getTokenBefore(body).range[1] === body.range[0] &&
+ !astUtils.canTokensBeAdjacent("do", sourceCode.getFirstToken(body, { skip: 1 }));
+
+ const openingBracket = sourceCode.getFirstToken(body);
+ const closingBracket = sourceCode.getLastToken(body);
+ const lastTokenInBlock = sourceCode.getTokenBefore(closingBracket);
+
+ if (needsSemicolon(closingBracket)) {
+
+ /*
+ * If removing braces would cause a SyntaxError due to multiple statements on the same line (or
+ * change the semantics of the code due to ASI), don't perform a fix.
+ */
+ return null;
+ }
+
+ const resultingBodyText = sourceCode.getText().slice(openingBracket.range[1], lastTokenInBlock.range[0]) +
+ sourceCode.getText(lastTokenInBlock) +
+ sourceCode.getText().slice(lastTokenInBlock.range[1], closingBracket.range[0]);
+
+ return fixer.replaceText(body, (needsPrecedingSpace ? " " : "") + resultingBodyText);
+ }
+ });
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Prepares to check the bodies of a "if", "else if" and "else" chain.
+ * @param {ASTNode} node The first IfStatement node of the chain.
+ * @returns {Object[]} prepared checks for each body of the chain. See `prepareCheck` for more
+ * information.
+ */
+ function prepareIfChecks(node) {
+ const preparedChecks = [];
+
+ for (let currentNode = node; currentNode; currentNode = currentNode.alternate) {
+ preparedChecks.push(prepareCheck(currentNode, currentNode.consequent, "if", { condition: true }));
+ if (currentNode.alternate && currentNode.alternate.type !== "IfStatement") {
+ preparedChecks.push(prepareCheck(currentNode, currentNode.alternate, "else"));
+ break;
+ }
+ }
+
+ if (consistent) {
+
+ /*
+ * If any node should have or already have braces, make sure they
+ * all have braces.
+ * If all nodes shouldn't have braces, make sure they don't.
+ */
+ const expected = preparedChecks.some(preparedCheck => {
+ if (preparedCheck.expected !== null) {
+ return preparedCheck.expected;
+ }
+ return preparedCheck.actual;
+ });
+
+ preparedChecks.forEach(preparedCheck => {
+ preparedCheck.expected = expected;
+ });
+ }
+
+ return preparedChecks;
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ return {
+ IfStatement(node) {
+ const parent = node.parent;
+ const isElseIf = parent.type === "IfStatement" && parent.alternate === node;
+
+ if (!isElseIf) {
+
+ // This is a top `if`, check the whole `if-else-if` chain
+ prepareIfChecks(node).forEach(preparedCheck => {
+ preparedCheck.check();
+ });
+ }
+
+ // Skip `else if`, it's already checked (when the top `if` was visited)
+ },
+
+ WhileStatement(node) {
+ prepareCheck(node, node.body, "while", { condition: true }).check();
+ },
+
+ DoWhileStatement(node) {
+ prepareCheck(node, node.body, "do").check();
+ },
+
+ ForStatement(node) {
+ prepareCheck(node, node.body, "for", { condition: true }).check();
+ },
+
+ ForInStatement(node) {
+ prepareCheck(node, node.body, "for-in").check();
+ },
+
+ ForOfStatement(node) {
+ prepareCheck(node, node.body, "for-of").check();
+ }
+ };
+ }
+};