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 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | var astUtils = require("../ast-utils");
|
13 |
|
14 | //------------------------------------------------------------------------------
|
15 | // Rule Definition
|
16 | //------------------------------------------------------------------------------
|
17 |
|
18 | module.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 |
|
262 | module.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 | };
|