UNPKG

9.93 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to flag statements without curly braces
3 * @author Nicholas C. Zakas
4 * @copyright 2015 Ivan Nikulin. All rights reserved.
5 */
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12var astUtils = require("../ast-utils");
13
14//------------------------------------------------------------------------------
15// Rule Definition
16//------------------------------------------------------------------------------
17
18module.exports = function(context) {
19
20 var multiOnly = (context.options[0] === "multi");
21 var multiLine = (context.options[0] === "multi-line");
22 var multiOrNest = (context.options[0] === "multi-or-nest");
23 var consistent = (context.options[1] === "consistent");
24
25 //--------------------------------------------------------------------------
26 // Helpers
27 //--------------------------------------------------------------------------
28
29 /**
30 * Determines if a given node is a one-liner that's on the same line as it's preceding code.
31 * @param {ASTNode} node The node to check.
32 * @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code.
33 * @private
34 */
35 function isCollapsedOneLiner(node) {
36 var before = context.getTokenBefore(node),
37 last = context.getLastToken(node);
38 return before.loc.start.line === last.loc.end.line;
39 }
40
41 /**
42 * Determines if a given node is a one-liner.
43 * @param {ASTNode} node The node to check.
44 * @returns {boolean} True if the node is a one-liner.
45 * @private
46 */
47 function isOneLiner(node) {
48 var first = context.getFirstToken(node),
49 last = context.getLastToken(node);
50
51 return first.loc.start.line === last.loc.end.line;
52 }
53
54 /**
55 * Gets the `else` keyword token of a given `IfStatement` node.
56 * @param {ASTNode} node - A `IfStatement` node to get.
57 * @returns {Token} The `else` keyword token.
58 */
59 function getElseKeyword(node) {
60 var sourceCode = context.getSourceCode();
61 var token = sourceCode.getTokenAfter(node.consequent);
62
63 while (token.type !== "Keyword" || token.value !== "else") {
64 token = sourceCode.getTokenAfter(token);
65 }
66
67 return token;
68 }
69
70 /**
71 * Checks a given IfStatement node requires braces of the consequent chunk.
72 * This returns `true` when below:
73 *
74 * 1. The given node has the `alternate` node.
75 * 2. There is a `IfStatement` which doesn't have `alternate` node in the
76 * trailing statement chain of the `consequent` node.
77 *
78 * @param {ASTNode} node - A IfStatement node to check.
79 * @returns {boolean} `true` if the node requires braces of the consequent chunk.
80 */
81 function requiresBraceOfConsequent(node) {
82 if (node.alternate && node.consequent.type === "BlockStatement") {
83 if (node.consequent.body.length >= 2) {
84 return true;
85 }
86
87 node = node.consequent.body[0];
88 while (node) {
89 if (node.type === "IfStatement" && !node.alternate) {
90 return true;
91 }
92 node = astUtils.getTrailingStatement(node);
93 }
94 }
95
96 return false;
97 }
98
99 /**
100 * Reports "Expected { after ..." error
101 * @param {ASTNode} node The node to report.
102 * @param {string} name The name to report.
103 * @param {string} suffix Additional string to add to the end of a report.
104 * @returns {void}
105 * @private
106 */
107 function reportExpectedBraceError(node, name, suffix) {
108 context.report({
109 node: node,
110 loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
111 message: "Expected { after '{{name}}'{{suffix}}.",
112 data: {
113 name: name,
114 suffix: (suffix ? " " + suffix : "")
115 }
116 });
117 }
118
119 /**
120 * Reports "Unnecessary { after ..." error
121 * @param {ASTNode} node The node to report.
122 * @param {string} name The name to report.
123 * @param {string} suffix Additional string to add to the end of a report.
124 * @returns {void}
125 * @private
126 */
127 function reportUnnecessaryBraceError(node, name, suffix) {
128 context.report({
129 node: node,
130 loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
131 message: "Unnecessary { after '{{name}}'{{suffix}}.",
132 data: {
133 name: name,
134 suffix: (suffix ? " " + suffix : "")
135 }
136 });
137 }
138
139 /**
140 * Prepares to check the body of a node to see if it's a block statement.
141 * @param {ASTNode} node The node to report if there's a problem.
142 * @param {ASTNode} body The body node to check for blocks.
143 * @param {string} name The name to report if there's a problem.
144 * @param {string} suffix Additional string to add to the end of a report.
145 * @returns {object} a prepared check object, with "actual", "expected", "check" properties.
146 * "actual" will be `true` or `false` whether the body is already a block statement.
147 * "expected" will be `true` or `false` if the body should be a block statement or not, or
148 * `null` if it doesn't matter, depending on the rule options. It can be modified to change
149 * the final behavior of "check".
150 * "check" will be a function reporting appropriate problems depending on the other
151 * properties.
152 */
153 function prepareCheck(node, body, name, suffix) {
154 var hasBlock = (body.type === "BlockStatement");
155 var expected = null;
156
157 if (node.type === "IfStatement" && node.consequent === body && requiresBraceOfConsequent(node)) {
158 expected = true;
159 } else if (multiOnly) {
160 if (hasBlock && body.body.length === 1) {
161 expected = false;
162 }
163 } else if (multiLine) {
164 if (!isCollapsedOneLiner(body)) {
165 expected = true;
166 }
167 } else if (multiOrNest) {
168 if (hasBlock && body.body.length === 1 && isOneLiner(body.body[0])) {
169 expected = false;
170 } else if (!isOneLiner(body)) {
171 expected = true;
172 }
173 } else {
174 expected = true;
175 }
176
177 return {
178 actual: hasBlock,
179 expected: expected,
180 check: function() {
181 if (this.expected !== null && this.expected !== this.actual) {
182 if (this.expected) {
183 reportExpectedBraceError(node, name, suffix);
184 } else {
185 reportUnnecessaryBraceError(node, name, suffix);
186 }
187 }
188 }
189 };
190 }
191
192 /**
193 * Prepares to check the bodies of a "if", "else if" and "else" chain.
194 * @param {ASTNode} node The first IfStatement node of the chain.
195 * @returns {object[]} prepared checks for each body of the chain. See `prepareCheck` for more
196 * information.
197 */
198 function prepareIfChecks(node) {
199 var preparedChecks = [];
200 do {
201 preparedChecks.push(prepareCheck(node, node.consequent, "if", "condition"));
202 if (node.alternate && node.alternate.type !== "IfStatement") {
203 preparedChecks.push(prepareCheck(node, node.alternate, "else"));
204 break;
205 }
206 node = node.alternate;
207 } while (node);
208
209 if (consistent) {
210 // If any node should have or already have braces, make sure they all have braces.
211 // If all nodes shouldn't have braces, make sure they don't.
212 var expected = preparedChecks.some(function(preparedCheck) {
213 if (preparedCheck.expected !== null) {
214 return preparedCheck.expected;
215 }
216 return preparedCheck.actual;
217 });
218
219 preparedChecks.forEach(function(preparedCheck) {
220 preparedCheck.expected = expected;
221 });
222 }
223
224 return preparedChecks;
225 }
226
227 //--------------------------------------------------------------------------
228 // Public
229 //--------------------------------------------------------------------------
230
231 return {
232 "IfStatement": function(node) {
233 if (node.parent.type !== "IfStatement") {
234 prepareIfChecks(node).forEach(function(preparedCheck) {
235 preparedCheck.check();
236 });
237 }
238 },
239
240 "WhileStatement": function(node) {
241 prepareCheck(node, node.body, "while", "condition").check();
242 },
243
244 "DoWhileStatement": function(node) {
245 prepareCheck(node, node.body, "do").check();
246 },
247
248 "ForStatement": function(node) {
249 prepareCheck(node, node.body, "for", "condition").check();
250 },
251
252 "ForInStatement": function(node) {
253 prepareCheck(node, node.body, "for-in").check();
254 },
255
256 "ForOfStatement": function(node) {
257 prepareCheck(node, node.body, "for-of").check();
258 }
259 };
260};
261
262module.exports.schema = {
263 "anyOf": [
264 {
265 "type": "array",
266 "items": [
267 {
268 "enum": ["all"]
269 }
270 ],
271 "minItems": 0,
272 "maxItems": 1
273 },
274 {
275 "type": "array",
276 "items": [
277 {
278 "enum": ["multi", "multi-line", "multi-or-nest"]
279 },
280 {
281 "enum": ["consistent"]
282 }
283 ],
284 "minItems": 0,
285 "maxItems": 2
286 }
287 ]
288};