UNPKG

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