UNPKG

15.7 kBJavaScriptView Raw
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("../ast-utils");
12
13//------------------------------------------------------------------------------
14// Rule Definition
15//------------------------------------------------------------------------------
16
17module.exports = {
18 meta: {
19 docs: {
20 description: "enforce consistent brace style for all control statements",
21 category: "Best Practices",
22 recommended: false,
23 url: "https://eslint.org/docs/rules/curly"
24 },
25
26 schema: {
27 anyOf: [
28 {
29 type: "array",
30 items: [
31 {
32 enum: ["all"]
33 }
34 ],
35 minItems: 0,
36 maxItems: 1
37 },
38 {
39 type: "array",
40 items: [
41 {
42 enum: ["multi", "multi-line", "multi-or-nest"]
43 },
44 {
45 enum: ["consistent"]
46 }
47 ],
48 minItems: 0,
49 maxItems: 2
50 }
51 ]
52 },
53
54 fixable: "code",
55
56 messages: {
57 missingCurlyAfter: "Expected { after '{{name}}'.",
58 missingCurlyAfterCondition: "Expected { after '{{name}}' condition.",
59 unexpectedCurlyAfter: "Unnecessary { after '{{name}}'.",
60 unexpectedCurlyAfterCondition: "Unnecessary { after '{{name}}' condition."
61 }
62 },
63
64 create(context) {
65
66 const multiOnly = (context.options[0] === "multi");
67 const multiLine = (context.options[0] === "multi-line");
68 const multiOrNest = (context.options[0] === "multi-or-nest");
69 const consistent = (context.options[1] === "consistent");
70
71 const sourceCode = context.getSourceCode();
72
73 //--------------------------------------------------------------------------
74 // Helpers
75 //--------------------------------------------------------------------------
76
77 /**
78 * Determines if a given node is a one-liner that's on the same line as it's preceding code.
79 * @param {ASTNode} node The node to check.
80 * @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code.
81 * @private
82 */
83 function isCollapsedOneLiner(node) {
84 const before = sourceCode.getTokenBefore(node);
85 const last = sourceCode.getLastToken(node);
86 const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last;
87
88 return before.loc.start.line === lastExcludingSemicolon.loc.end.line;
89 }
90
91 /**
92 * Determines if a given node is a one-liner.
93 * @param {ASTNode} node The node to check.
94 * @returns {boolean} True if the node is a one-liner.
95 * @private
96 */
97 function isOneLiner(node) {
98 const first = sourceCode.getFirstToken(node),
99 last = sourceCode.getLastToken(node);
100
101 return first.loc.start.line === last.loc.end.line;
102 }
103
104 /**
105 * Checks if the given token is an `else` token or not.
106 *
107 * @param {Token} token - The token to check.
108 * @returns {boolean} `true` if the token is an `else` token.
109 */
110 function isElseKeywordToken(token) {
111 return token.value === "else" && token.type === "Keyword";
112 }
113
114 /**
115 * Gets the `else` keyword token of a given `IfStatement` node.
116 * @param {ASTNode} node - A `IfStatement` node to get.
117 * @returns {Token} The `else` keyword token.
118 */
119 function getElseKeyword(node) {
120 return node.alternate && sourceCode.getFirstTokenBetween(node.consequent, node.alternate, isElseKeywordToken);
121 }
122
123 /**
124 * Checks a given IfStatement node requires braces of the consequent chunk.
125 * This returns `true` when below:
126 *
127 * 1. The given node has the `alternate` node.
128 * 2. There is a `IfStatement` which doesn't have `alternate` node in the
129 * trailing statement chain of the `consequent` node.
130 *
131 * @param {ASTNode} node - A IfStatement node to check.
132 * @returns {boolean} `true` if the node requires braces of the consequent chunk.
133 */
134 function requiresBraceOfConsequent(node) {
135 if (node.alternate && node.consequent.type === "BlockStatement") {
136 if (node.consequent.body.length >= 2) {
137 return true;
138 }
139
140 node = node.consequent.body[0];
141 while (node) {
142 if (node.type === "IfStatement" && !node.alternate) {
143 return true;
144 }
145 node = astUtils.getTrailingStatement(node);
146 }
147 }
148
149 return false;
150 }
151
152 /**
153 * Determines if a semicolon needs to be inserted after removing a set of curly brackets, in order to avoid a SyntaxError.
154 * @param {Token} closingBracket The } token
155 * @returns {boolean} `true` if a semicolon needs to be inserted after the last statement in the block.
156 */
157 function needsSemicolon(closingBracket) {
158 const tokenBefore = sourceCode.getTokenBefore(closingBracket);
159 const tokenAfter = sourceCode.getTokenAfter(closingBracket);
160 const lastBlockNode = sourceCode.getNodeByRangeIndex(tokenBefore.range[0]);
161
162 if (astUtils.isSemicolonToken(tokenBefore)) {
163
164 // If the last statement already has a semicolon, don't add another one.
165 return false;
166 }
167
168 if (!tokenAfter) {
169
170 // If there are no statements after this block, there is no need to add a semicolon.
171 return false;
172 }
173
174 if (lastBlockNode.type === "BlockStatement" && lastBlockNode.parent.type !== "FunctionExpression" && lastBlockNode.parent.type !== "ArrowFunctionExpression") {
175
176 /*
177 * If the last node surrounded by curly brackets is a BlockStatement (other than a FunctionExpression or an ArrowFunctionExpression),
178 * don't insert a semicolon. Otherwise, the semicolon would be parsed as a separate statement, which would cause
179 * a SyntaxError if it was followed by `else`.
180 */
181 return false;
182 }
183
184 if (tokenBefore.loc.end.line === tokenAfter.loc.start.line) {
185
186 // If the next token is on the same line, insert a semicolon.
187 return true;
188 }
189
190 if (/^[([/`+-]/.test(tokenAfter.value)) {
191
192 // If the next token starts with a character that would disrupt ASI, insert a semicolon.
193 return true;
194 }
195
196 if (tokenBefore.type === "Punctuator" && (tokenBefore.value === "++" || tokenBefore.value === "--")) {
197
198 // If the last token is ++ or --, insert a semicolon to avoid disrupting ASI.
199 return true;
200 }
201
202 // Otherwise, do not insert a semicolon.
203 return false;
204 }
205
206 /**
207 * Prepares to check the body of a node to see if it's a block statement.
208 * @param {ASTNode} node The node to report if there's a problem.
209 * @param {ASTNode} body The body node to check for blocks.
210 * @param {string} name The name to report if there's a problem.
211 * @param {{ condition: boolean }} opts Options to pass to the report functions
212 * @returns {Object} a prepared check object, with "actual", "expected", "check" properties.
213 * "actual" will be `true` or `false` whether the body is already a block statement.
214 * "expected" will be `true` or `false` if the body should be a block statement or not, or
215 * `null` if it doesn't matter, depending on the rule options. It can be modified to change
216 * the final behavior of "check".
217 * "check" will be a function reporting appropriate problems depending on the other
218 * properties.
219 */
220 function prepareCheck(node, body, name, opts) {
221 const hasBlock = (body.type === "BlockStatement");
222 let expected = null;
223
224 if (node.type === "IfStatement" && node.consequent === body && requiresBraceOfConsequent(node)) {
225 expected = true;
226 } else if (multiOnly) {
227 if (hasBlock && body.body.length === 1) {
228 expected = false;
229 }
230 } else if (multiLine) {
231 if (!isCollapsedOneLiner(body)) {
232 expected = true;
233 }
234 } else if (multiOrNest) {
235 if (hasBlock && body.body.length === 1 && isOneLiner(body.body[0])) {
236 const leadingComments = sourceCode.getCommentsBefore(body.body[0]);
237
238 expected = leadingComments.length > 0;
239 } else if (!isOneLiner(body)) {
240 expected = true;
241 }
242 } else {
243 expected = true;
244 }
245
246 return {
247 actual: hasBlock,
248 expected,
249 check() {
250 if (this.expected !== null && this.expected !== this.actual) {
251 if (this.expected) {
252 context.report({
253 node,
254 loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
255 messageId: opts && opts.condition ? "missingCurlyAfterCondition" : "missingCurlyAfter",
256 data: {
257 name
258 },
259 fix: fixer => fixer.replaceText(body, `{${sourceCode.getText(body)}}`)
260 });
261 } else {
262 context.report({
263 node,
264 loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
265 messageId: opts && opts.condition ? "unexpectedCurlyAfterCondition" : "unexpectedCurlyAfter",
266 data: {
267 name
268 },
269 fix(fixer) {
270
271 /*
272 * `do while` expressions sometimes need a space to be inserted after `do`.
273 * e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)`
274 */
275 const needsPrecedingSpace = node.type === "DoWhileStatement" &&
276 sourceCode.getTokenBefore(body).range[1] === body.range[0] &&
277 !astUtils.canTokensBeAdjacent("do", sourceCode.getFirstToken(body, { skip: 1 }));
278
279 const openingBracket = sourceCode.getFirstToken(body);
280 const closingBracket = sourceCode.getLastToken(body);
281 const lastTokenInBlock = sourceCode.getTokenBefore(closingBracket);
282
283 if (needsSemicolon(closingBracket)) {
284
285 /*
286 * If removing braces would cause a SyntaxError due to multiple statements on the same line (or
287 * change the semantics of the code due to ASI), don't perform a fix.
288 */
289 return null;
290 }
291
292 const resultingBodyText = sourceCode.getText().slice(openingBracket.range[1], lastTokenInBlock.range[0]) +
293 sourceCode.getText(lastTokenInBlock) +
294 sourceCode.getText().slice(lastTokenInBlock.range[1], closingBracket.range[0]);
295
296 return fixer.replaceText(body, (needsPrecedingSpace ? " " : "") + resultingBodyText);
297 }
298 });
299 }
300 }
301 }
302 };
303 }
304
305 /**
306 * Prepares to check the bodies of a "if", "else if" and "else" chain.
307 * @param {ASTNode} node The first IfStatement node of the chain.
308 * @returns {Object[]} prepared checks for each body of the chain. See `prepareCheck` for more
309 * information.
310 */
311 function prepareIfChecks(node) {
312 const preparedChecks = [];
313
314 do {
315 preparedChecks.push(prepareCheck(node, node.consequent, "if", { condition: true }));
316 if (node.alternate && node.alternate.type !== "IfStatement") {
317 preparedChecks.push(prepareCheck(node, node.alternate, "else"));
318 break;
319 }
320 node = node.alternate;
321 } while (node);
322
323 if (consistent) {
324
325 /*
326 * If any node should have or already have braces, make sure they
327 * all have braces.
328 * If all nodes shouldn't have braces, make sure they don't.
329 */
330 const expected = preparedChecks.some(preparedCheck => {
331 if (preparedCheck.expected !== null) {
332 return preparedCheck.expected;
333 }
334 return preparedCheck.actual;
335 });
336
337 preparedChecks.forEach(preparedCheck => {
338 preparedCheck.expected = expected;
339 });
340 }
341
342 return preparedChecks;
343 }
344
345 //--------------------------------------------------------------------------
346 // Public
347 //--------------------------------------------------------------------------
348
349 return {
350 IfStatement(node) {
351 if (node.parent.type !== "IfStatement") {
352 prepareIfChecks(node).forEach(preparedCheck => {
353 preparedCheck.check();
354 });
355 }
356 },
357
358 WhileStatement(node) {
359 prepareCheck(node, node.body, "while", { condition: true }).check();
360 },
361
362 DoWhileStatement(node) {
363 prepareCheck(node, node.body, "do").check();
364 },
365
366 ForStatement(node) {
367 prepareCheck(node, node.body, "for", { condition: true }).check();
368 },
369
370 ForInStatement(node) {
371 prepareCheck(node, node.body, "for-in").check();
372 },
373
374 ForOfStatement(node) {
375 prepareCheck(node, node.body, "for-of").check();
376 }
377 };
378 }
379};