]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/curly.js
92d31a6476e4359ed4fda9b0e3622d33eb1b0b77
[pve-eslint.git] / eslint / lib / rules / curly.js
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
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 };