• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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