1 | /**
|
2 | * @fileoverview Rule to flag statements without curly braces
|
3 | * @author Nicholas C. Zakas
|
4 | */
|
5 | ;
|
6 |
|
7 | //------------------------------------------------------------------------------
|
8 | // Requirements
|
9 | //------------------------------------------------------------------------------
|
10 |
|
11 | const astUtils = require("./utils/ast-utils");
|
12 |
|
13 | //------------------------------------------------------------------------------
|
14 | // Rule Definition
|
15 | //------------------------------------------------------------------------------
|
16 |
|
17 | module.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 | const parent = node.parent;
|
461 | const isElseIf = parent.type === "IfStatement" && parent.alternate === node;
|
462 |
|
463 | if (!isElseIf) {
|
464 |
|
465 | // This is a top `if`, check the whole `if-else-if` chain
|
466 | prepareIfChecks(node).forEach(preparedCheck => {
|
467 | preparedCheck.check();
|
468 | });
|
469 | }
|
470 |
|
471 | // Skip `else if`, it's already checked (when the top `if` was visited)
|
472 | },
|
473 |
|
474 | WhileStatement(node) {
|
475 | prepareCheck(node, node.body, "while", { condition: true }).check();
|
476 | },
|
477 |
|
478 | DoWhileStatement(node) {
|
479 | prepareCheck(node, node.body, "do").check();
|
480 | },
|
481 |
|
482 | ForStatement(node) {
|
483 | prepareCheck(node, node.body, "for", { condition: true }).check();
|
484 | },
|
485 |
|
486 | ForInStatement(node) {
|
487 | prepareCheck(node, node.body, "for-in").check();
|
488 | },
|
489 |
|
490 | ForOfStatement(node) {
|
491 | prepareCheck(node, node.body, "for-of").check();
|
492 | }
|
493 | };
|
494 | }
|
495 | };
|