UNPKG

17.9 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to enforce spacing before and after keywords.
3 * @author Toru Nagashima
4 * @copyright 2015 Toru Nagashima. All rights reserved.
5 * See LICENSE file in root directory for full license.
6 */
7
8"use strict";
9
10//------------------------------------------------------------------------------
11// Requirements
12//------------------------------------------------------------------------------
13
14var astUtils = require("../ast-utils"),
15 keywords = require("../util/keywords");
16
17//------------------------------------------------------------------------------
18// Constants
19//------------------------------------------------------------------------------
20
21var PREV_TOKEN = /^[\)\]\}>]$/;
22var NEXT_TOKEN = /^(?:[\(\[\{<~!]|\+\+?|--?)$/;
23var PREV_TOKEN_M = /^[\)\]\}>*]$/;
24var NEXT_TOKEN_M = /^[\{*]$/;
25var TEMPLATE_OPEN_PAREN = /\$\{$/;
26var TEMPLATE_CLOSE_PAREN = /^\}/;
27var CHECK_TYPE = /^(?:JSXElement|RegularExpression|String|Template)$/;
28var KEYS = keywords.concat(["as", "await", "from", "get", "let", "of", "set", "yield"]);
29
30// check duplications.
31(function() {
32 KEYS.sort();
33 for (var i = 1; i < KEYS.length; ++i) {
34 if (KEYS[i] === KEYS[i - 1]) {
35 throw new Error("Duplication was found in the keyword list: " + KEYS[i]);
36 }
37 }
38}());
39
40//------------------------------------------------------------------------------
41// Helpers
42//------------------------------------------------------------------------------
43
44/**
45 * Checks whether or not a given token is a "Template" token ends with "${".
46 *
47 * @param {Token} token - A token to check.
48 * @returns {boolean} `true` if the token is a "Template" token ends with "${".
49 */
50function isOpenParenOfTemplate(token) {
51 return token.type === "Template" && TEMPLATE_OPEN_PAREN.test(token.value);
52}
53
54/**
55 * Checks whether or not a given token is a "Template" token starts with "}".
56 *
57 * @param {Token} token - A token to check.
58 * @returns {boolean} `true` if the token is a "Template" token starts with "}".
59 */
60function isCloseParenOfTemplate(token) {
61 return token.type === "Template" && TEMPLATE_CLOSE_PAREN.test(token.value);
62}
63
64//------------------------------------------------------------------------------
65// Rule Definition
66//------------------------------------------------------------------------------
67
68module.exports = function(context) {
69 var sourceCode = context.getSourceCode();
70
71 /**
72 * Reports a given token if there are not space(s) before the token.
73 *
74 * @param {Token} token - A token to report.
75 * @param {RegExp|undefined} pattern - Optional. A pattern of the previous
76 * token to check.
77 * @returns {void}
78 */
79 function expectSpaceBefore(token, pattern) {
80 pattern = pattern || PREV_TOKEN;
81
82 var prevToken = sourceCode.getTokenBefore(token);
83 if (prevToken &&
84 (CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
85 !isOpenParenOfTemplate(prevToken) &&
86 astUtils.isTokenOnSameLine(prevToken, token) &&
87 !sourceCode.isSpaceBetweenTokens(prevToken, token)
88 ) {
89 context.report({
90 loc: token.loc.start,
91 message: "Expected space(s) before \"{{value}}\".",
92 data: token,
93 fix: function(fixer) {
94 return fixer.insertTextBefore(token, " ");
95 }
96 });
97 }
98 }
99
100 /**
101 * Reports a given token if there are space(s) before the token.
102 *
103 * @param {Token} token - A token to report.
104 * @param {RegExp|undefined} pattern - Optional. A pattern of the previous
105 * token to check.
106 * @returns {void}
107 */
108 function unexpectSpaceBefore(token, pattern) {
109 pattern = pattern || PREV_TOKEN;
110
111 var prevToken = sourceCode.getTokenBefore(token);
112 if (prevToken &&
113 (CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
114 !isOpenParenOfTemplate(prevToken) &&
115 astUtils.isTokenOnSameLine(prevToken, token) &&
116 sourceCode.isSpaceBetweenTokens(prevToken, token)
117 ) {
118 context.report({
119 loc: token.loc.start,
120 message: "Unexpected space(s) before \"{{value}}\".",
121 data: token,
122 fix: function(fixer) {
123 return fixer.removeRange([prevToken.range[1], token.range[0]]);
124 }
125 });
126 }
127 }
128
129 /**
130 * Reports a given token if there are not space(s) after the token.
131 *
132 * @param {Token} token - A token to report.
133 * @param {RegExp|undefined} pattern - Optional. A pattern of the next
134 * token to check.
135 * @returns {void}
136 */
137 function expectSpaceAfter(token, pattern) {
138 pattern = pattern || NEXT_TOKEN;
139
140 var nextToken = sourceCode.getTokenAfter(token);
141 if (nextToken &&
142 (CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
143 !isCloseParenOfTemplate(nextToken) &&
144 astUtils.isTokenOnSameLine(token, nextToken) &&
145 !sourceCode.isSpaceBetweenTokens(token, nextToken)
146 ) {
147 context.report({
148 loc: token.loc.start,
149 message: "Expected space(s) after \"{{value}}\".",
150 data: token,
151 fix: function(fixer) {
152 return fixer.insertTextAfter(token, " ");
153 }
154 });
155 }
156 }
157
158 /**
159 * Reports a given token if there are space(s) after the token.
160 *
161 * @param {Token} token - A token to report.
162 * @param {RegExp|undefined} pattern - Optional. A pattern of the next
163 * token to check.
164 * @returns {void}
165 */
166 function unexpectSpaceAfter(token, pattern) {
167 pattern = pattern || NEXT_TOKEN;
168
169 var nextToken = sourceCode.getTokenAfter(token);
170 if (nextToken &&
171 (CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
172 !isCloseParenOfTemplate(nextToken) &&
173 astUtils.isTokenOnSameLine(token, nextToken) &&
174 sourceCode.isSpaceBetweenTokens(token, nextToken)
175 ) {
176 context.report({
177 loc: token.loc.start,
178 message: "Unexpected space(s) after \"{{value}}\".",
179 data: token,
180 fix: function(fixer) {
181 return fixer.removeRange([token.range[1], nextToken.range[0]]);
182 }
183 });
184 }
185 }
186
187 /**
188 * Parses the option object and determines check methods for each keyword.
189 *
190 * @param {object|undefined} options - The option object to parse.
191 * @returns {object} - Normalized option object.
192 * Keys are keywords (there are for every keyword).
193 * Values are instances of `{"before": function, "after": function}`.
194 */
195 function parseOptions(options) {
196 var before = !options || options.before !== false;
197 var after = !options || options.after !== false;
198 var defaultValue = {
199 before: before ? expectSpaceBefore : unexpectSpaceBefore,
200 after: after ? expectSpaceAfter : unexpectSpaceAfter
201 };
202 var overrides = (options && options.overrides) || {};
203 var retv = Object.create(null);
204
205 for (var i = 0; i < KEYS.length; ++i) {
206 var key = KEYS[i];
207 var override = overrides[key];
208
209 if (override) {
210 var thisBefore = ("before" in override) ? override.before : before;
211 var thisAfter = ("after" in override) ? override.after : after;
212 retv[key] = {
213 before: thisBefore ? expectSpaceBefore : unexpectSpaceBefore,
214 after: thisAfter ? expectSpaceAfter : unexpectSpaceAfter
215 };
216 } else {
217 retv[key] = defaultValue;
218 }
219 }
220
221 return retv;
222 }
223
224 var checkMethodMap = parseOptions(context.options[0]);
225
226 /**
227 * Reports a given token if usage of spacing followed by the token is
228 * invalid.
229 *
230 * @param {Token} token - A token to report.
231 * @param {RegExp|undefined} pattern - Optional. A pattern of the previous
232 * token to check.
233 * @returns {void}
234 */
235 function checkSpacingBefore(token, pattern) {
236 checkMethodMap[token.value].before(token, pattern);
237 }
238
239 /**
240 * Reports a given token if usage of spacing preceded by the token is
241 * invalid.
242 *
243 * @param {Token} token - A token to report.
244 * @param {RegExp|undefined} pattern - Optional. A pattern of the next
245 * token to check.
246 * @returns {void}
247 */
248 function checkSpacingAfter(token, pattern) {
249 checkMethodMap[token.value].after(token, pattern);
250 }
251
252 /**
253 * Reports a given token if usage of spacing around the token is invalid.
254 *
255 * @param {Token} token - A token to report.
256 * @returns {void}
257 */
258 function checkSpacingAround(token) {
259 checkSpacingBefore(token);
260 checkSpacingAfter(token);
261 }
262
263 /**
264 * Reports the first token of a given node if the first token is a keyword
265 * and usage of spacing around the token is invalid.
266 *
267 * @param {ASTNode|null} node - A node to report.
268 * @returns {void}
269 */
270 function checkSpacingAroundFirstToken(node) {
271 var firstToken = node && sourceCode.getFirstToken(node);
272 if (firstToken && firstToken.type === "Keyword") {
273 checkSpacingAround(firstToken);
274 }
275 }
276
277 /**
278 * Reports the first token of a given node if the first token is a keyword
279 * and usage of spacing followed by the token is invalid.
280 *
281 * This is used for unary operators (e.g. `typeof`), `function`, and `super`.
282 * Other rules are handling usage of spacing preceded by those keywords.
283 *
284 * @param {ASTNode|null} node - A node to report.
285 * @returns {void}
286 */
287 function checkSpacingBeforeFirstToken(node) {
288 var firstToken = node && sourceCode.getFirstToken(node);
289 if (firstToken && firstToken.type === "Keyword") {
290 checkSpacingBefore(firstToken);
291 }
292 }
293
294 /**
295 * Reports the previous token of a given node if the token is a keyword and
296 * usage of spacing around the token is invalid.
297 *
298 * @param {ASTNode|null} node - A node to report.
299 * @returns {void}
300 */
301 function checkSpacingAroundTokenBefore(node) {
302 if (node) {
303 var token = sourceCode.getTokenBefore(node);
304 while (token.type !== "Keyword") {
305 token = sourceCode.getTokenBefore(token);
306 }
307
308 checkSpacingAround(token);
309 }
310 }
311
312 /**
313 * Reports `class` and `extends` keywords of a given node if usage of
314 * spacing around those keywords is invalid.
315 *
316 * @param {ASTNode} node - A node to report.
317 * @returns {void}
318 */
319 function checkSpacingForClass(node) {
320 checkSpacingAroundFirstToken(node);
321 checkSpacingAroundTokenBefore(node.superClass);
322 }
323
324 /**
325 * Reports `if` and `else` keywords of a given node if usage of spacing
326 * around those keywords is invalid.
327 *
328 * @param {ASTNode} node - A node to report.
329 * @returns {void}
330 */
331 function checkSpacingForIfStatement(node) {
332 checkSpacingAroundFirstToken(node);
333 checkSpacingAroundTokenBefore(node.alternate);
334 }
335
336 /**
337 * Reports `try`, `catch`, and `finally` keywords of a given node if usage
338 * of spacing around those keywords is invalid.
339 *
340 * @param {ASTNode} node - A node to report.
341 * @returns {void}
342 */
343 function checkSpacingForTryStatement(node) {
344 checkSpacingAroundFirstToken(node);
345 checkSpacingAroundFirstToken(node.handler);
346 checkSpacingAroundTokenBefore(node.finalizer);
347 }
348
349 /**
350 * Reports `do` and `while` keywords of a given node if usage of spacing
351 * around those keywords is invalid.
352 *
353 * @param {ASTNode} node - A node to report.
354 * @returns {void}
355 */
356 function checkSpacingForDoWhileStatement(node) {
357 checkSpacingAroundFirstToken(node);
358 checkSpacingAroundTokenBefore(node.test);
359 }
360
361 /**
362 * Reports `for` and `in` keywords of a given node if usage of spacing
363 * around those keywords is invalid.
364 *
365 * @param {ASTNode} node - A node to report.
366 * @returns {void}
367 */
368 function checkSpacingForForInStatement(node) {
369 checkSpacingAroundFirstToken(node);
370 checkSpacingAroundTokenBefore(node.right);
371 }
372
373 /**
374 * Reports `for` and `of` keywords of a given node if usage of spacing
375 * around those keywords is invalid.
376 *
377 * @param {ASTNode} node - A node to report.
378 * @returns {void}
379 */
380 function checkSpacingForForOfStatement(node) {
381 checkSpacingAroundFirstToken(node);
382
383 // `of` is not a keyword token.
384 var token = sourceCode.getTokenBefore(node.right);
385 while (token.value !== "of") {
386 token = sourceCode.getTokenBefore(token);
387 }
388 checkSpacingAround(token);
389 }
390
391 /**
392 * Reports `import`, `export`, `as`, and `from` keywords of a given node if
393 * usage of spacing around those keywords is invalid.
394 *
395 * This rule handles the `*` token in module declarations.
396 *
397 * import*as A from "./a"; /*error Expected space(s) after "import".
398 * error Expected space(s) before "as".
399 *
400 * @param {ASTNode} node - A node to report.
401 * @returns {void}
402 */
403 function checkSpacingForModuleDeclaration(node) {
404 var firstToken = sourceCode.getFirstToken(node);
405 checkSpacingBefore(firstToken, PREV_TOKEN_M);
406 checkSpacingAfter(firstToken, NEXT_TOKEN_M);
407
408 if (node.source) {
409 var fromToken = sourceCode.getTokenBefore(node.source);
410 checkSpacingBefore(fromToken, PREV_TOKEN_M);
411 checkSpacingAfter(fromToken, NEXT_TOKEN_M);
412 }
413 }
414
415 /**
416 * Reports `as` keyword of a given node if usage of spacing around this
417 * keyword is invalid.
418 *
419 * @param {ASTNode} node - A node to report.
420 * @returns {void}
421 */
422 function checkSpacingForImportNamespaceSpecifier(node) {
423 var asToken = sourceCode.getFirstToken(node, 1);
424 checkSpacingBefore(asToken, PREV_TOKEN_M);
425 }
426
427 /**
428 * Reports `static`, `get`, and `set` keywords of a given node if usage of
429 * spacing around those keywords is invalid.
430 *
431 * @param {ASTNode} node - A node to report.
432 * @returns {void}
433 */
434 function checkSpacingForProperty(node) {
435 if (node.static) {
436 checkSpacingAroundFirstToken(node);
437 }
438 if (node.kind === "get" || node.kind === "set") {
439 var token = sourceCode.getFirstToken(
440 node,
441 node.static ? 1 : 0
442 );
443 checkSpacingAround(token);
444 }
445 }
446
447 return {
448 // Statements
449 DebuggerStatement: checkSpacingAroundFirstToken,
450 WithStatement: checkSpacingAroundFirstToken,
451
452 // Statements - Control flow
453 BreakStatement: checkSpacingAroundFirstToken,
454 ContinueStatement: checkSpacingAroundFirstToken,
455 ReturnStatement: checkSpacingAroundFirstToken,
456 ThrowStatement: checkSpacingAroundFirstToken,
457 TryStatement: checkSpacingForTryStatement,
458
459 // Statements - Choice
460 IfStatement: checkSpacingForIfStatement,
461 SwitchStatement: checkSpacingAroundFirstToken,
462 SwitchCase: checkSpacingAroundFirstToken,
463
464 // Statements - Loops
465 DoWhileStatement: checkSpacingForDoWhileStatement,
466 ForInStatement: checkSpacingForForInStatement,
467 ForOfStatement: checkSpacingForForOfStatement,
468 ForStatement: checkSpacingAroundFirstToken,
469 WhileStatement: checkSpacingAroundFirstToken,
470
471 // Statements - Declarations
472 ClassDeclaration: checkSpacingForClass,
473 ExportNamedDeclaration: checkSpacingForModuleDeclaration,
474 ExportDefaultDeclaration: checkSpacingAroundFirstToken,
475 ExportAllDeclaration: checkSpacingForModuleDeclaration,
476 FunctionDeclaration: checkSpacingBeforeFirstToken,
477 ImportDeclaration: checkSpacingForModuleDeclaration,
478 VariableDeclaration: checkSpacingAroundFirstToken,
479
480 // Expressions
481 ClassExpression: checkSpacingForClass,
482 FunctionExpression: checkSpacingBeforeFirstToken,
483 NewExpression: checkSpacingBeforeFirstToken,
484 Super: checkSpacingBeforeFirstToken,
485 ThisExpression: checkSpacingBeforeFirstToken,
486 UnaryExpression: checkSpacingBeforeFirstToken,
487 YieldExpression: checkSpacingBeforeFirstToken,
488
489 // Others
490 ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier,
491 MethodDefinition: checkSpacingForProperty,
492 Property: checkSpacingForProperty
493 };
494};
495
496module.exports.schema = [
497 {
498 "type": "object",
499 "properties": {
500 "before": {"type": "boolean"},
501 "after": {"type": "boolean"},
502 "overrides": {
503 "type": "object",
504 "properties": KEYS.reduce(function(retv, key) {
505 retv[key] = {
506 "type": "object",
507 "properties": {
508 "before": {"type": "boolean"},
509 "after": {"type": "boolean"}
510 },
511 "additionalProperties": false
512 };
513 return retv;
514 }, {}),
515 "additionalProperties": false
516 }
517 },
518 "additionalProperties": false
519 }
520];