UNPKG

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