2 * @fileoverview Rule to enforce spacing before and after keywords.
3 * @author Toru Nagashima
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils
= require("./utils/ast-utils"),
13 keywords
= require("./utils/keywords");
15 //------------------------------------------------------------------------------
17 //------------------------------------------------------------------------------
19 const PREV_TOKEN
= /^[)\]}>]$/u;
20 const NEXT_TOKEN
= /^(?:[([{<~!]|\+\+?|--?)$/u;
21 const PREV_TOKEN_M
= /^[)\]}>*]$/u;
22 const NEXT_TOKEN_M
= /^[{*]$/u;
23 const TEMPLATE_OPEN_PAREN
= /\$\{$/u;
24 const TEMPLATE_CLOSE_PAREN
= /^\}/u;
25 const CHECK_TYPE
= /^(?:JSXElement|RegularExpression|String|Template|PrivateIdentifier)$/u;
26 const KEYS
= keywords
.concat(["as", "async", "await", "from", "get", "let", "of", "set", "yield"]);
28 // check duplications.
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]}`);
38 //------------------------------------------------------------------------------
40 //------------------------------------------------------------------------------
43 * Checks whether or not a given token is a "Template" token ends with "${".
44 * @param {Token} token A token to check.
45 * @returns {boolean} `true` if the token is a "Template" token ends with "${".
47 function isOpenParenOfTemplate(token
) {
48 return token
.type
=== "Template" && TEMPLATE_OPEN_PAREN
.test(token
.value
);
52 * Checks whether or not a given token is a "Template" token starts with "}".
53 * @param {Token} token A token to check.
54 * @returns {boolean} `true` if the token is a "Template" token starts with "}".
56 function isCloseParenOfTemplate(token
) {
57 return token
.type
=== "Template" && TEMPLATE_CLOSE_PAREN
.test(token
.value
);
60 //------------------------------------------------------------------------------
62 //------------------------------------------------------------------------------
64 /** @type {import('../shared/types').Rule} */
70 description
: "Enforce consistent spacing before and after keywords",
72 url
: "https://eslint.org/docs/latest/rules/keyword-spacing"
75 fixable
: "whitespace",
81 before
: { type
: "boolean", default: true },
82 after
: { type
: "boolean", default: true },
85 properties
: KEYS
.reduce((retv
, key
) => {
89 before
: { type
: "boolean" },
90 after
: { type
: "boolean" }
92 additionalProperties
: false
96 additionalProperties
: false
99 additionalProperties
: false
103 expectedBefore
: "Expected space(s) before \"{{value}}\".",
104 expectedAfter
: "Expected space(s) after \"{{value}}\".",
105 unexpectedBefore
: "Unexpected space(s) before \"{{value}}\".",
106 unexpectedAfter
: "Unexpected space(s) after \"{{value}}\"."
111 const sourceCode
= context
.sourceCode
;
113 const tokensToIgnore
= new WeakSet();
116 * Reports a given token if there are not space(s) before the token.
117 * @param {Token} token A token to report.
118 * @param {RegExp} pattern A pattern of the previous token to check.
121 function expectSpaceBefore(token
, pattern
) {
122 const prevToken
= sourceCode
.getTokenBefore(token
);
125 (CHECK_TYPE
.test(prevToken
.type
) || pattern
.test(prevToken
.value
)) &&
126 !isOpenParenOfTemplate(prevToken
) &&
127 !tokensToIgnore
.has(prevToken
) &&
128 astUtils
.isTokenOnSameLine(prevToken
, token
) &&
129 !sourceCode
.isSpaceBetweenTokens(prevToken
, token
)
133 messageId
: "expectedBefore",
136 return fixer
.insertTextBefore(token
, " ");
143 * Reports a given token if there are space(s) before the token.
144 * @param {Token} token A token to report.
145 * @param {RegExp} pattern A pattern of the previous token to check.
148 function unexpectSpaceBefore(token
, pattern
) {
149 const prevToken
= sourceCode
.getTokenBefore(token
);
152 (CHECK_TYPE
.test(prevToken
.type
) || pattern
.test(prevToken
.value
)) &&
153 !isOpenParenOfTemplate(prevToken
) &&
154 !tokensToIgnore
.has(prevToken
) &&
155 astUtils
.isTokenOnSameLine(prevToken
, token
) &&
156 sourceCode
.isSpaceBetweenTokens(prevToken
, token
)
159 loc
: { start
: prevToken
.loc
.end
, end
: token
.loc
.start
},
160 messageId
: "unexpectedBefore",
163 return fixer
.removeRange([prevToken
.range
[1], token
.range
[0]]);
170 * Reports a given token if there are not space(s) after the token.
171 * @param {Token} token A token to report.
172 * @param {RegExp} pattern A pattern of the next token to check.
175 function expectSpaceAfter(token
, pattern
) {
176 const nextToken
= sourceCode
.getTokenAfter(token
);
179 (CHECK_TYPE
.test(nextToken
.type
) || pattern
.test(nextToken
.value
)) &&
180 !isCloseParenOfTemplate(nextToken
) &&
181 !tokensToIgnore
.has(nextToken
) &&
182 astUtils
.isTokenOnSameLine(token
, nextToken
) &&
183 !sourceCode
.isSpaceBetweenTokens(token
, nextToken
)
187 messageId
: "expectedAfter",
190 return fixer
.insertTextAfter(token
, " ");
197 * Reports a given token if there are space(s) after the token.
198 * @param {Token} token A token to report.
199 * @param {RegExp} pattern A pattern of the next token to check.
202 function unexpectSpaceAfter(token
, pattern
) {
203 const nextToken
= sourceCode
.getTokenAfter(token
);
206 (CHECK_TYPE
.test(nextToken
.type
) || pattern
.test(nextToken
.value
)) &&
207 !isCloseParenOfTemplate(nextToken
) &&
208 !tokensToIgnore
.has(nextToken
) &&
209 astUtils
.isTokenOnSameLine(token
, nextToken
) &&
210 sourceCode
.isSpaceBetweenTokens(token
, nextToken
)
214 loc
: { start
: token
.loc
.end
, end
: nextToken
.loc
.start
},
215 messageId
: "unexpectedAfter",
218 return fixer
.removeRange([token
.range
[1], nextToken
.range
[0]]);
225 * Parses the option object and determines check methods for each keyword.
226 * @param {Object|undefined} options The option object to parse.
227 * @returns {Object} - Normalized option object.
228 * Keys are keywords (there are for every keyword).
229 * Values are instances of `{"before": function, "after": function}`.
231 function parseOptions(options
= {}) {
232 const before
= options
.before
!== false;
233 const after
= options
.after
!== false;
234 const defaultValue
= {
235 before
: before
? expectSpaceBefore
: unexpectSpaceBefore
,
236 after
: after
? expectSpaceAfter
: unexpectSpaceAfter
238 const overrides
= (options
&& options
.overrides
) || {};
239 const retv
= Object
.create(null);
241 for (let i
= 0; i
< KEYS
.length
; ++i
) {
243 const override
= overrides
[key
];
246 const thisBefore
= ("before" in override
) ? override
.before
: before
;
247 const thisAfter
= ("after" in override
) ? override
.after
: after
;
250 before
: thisBefore
? expectSpaceBefore
: unexpectSpaceBefore
,
251 after
: thisAfter
? expectSpaceAfter
: unexpectSpaceAfter
254 retv
[key
] = defaultValue
;
261 const checkMethodMap
= parseOptions(context
.options
[0]);
264 * Reports a given token if usage of spacing followed by the token is
266 * @param {Token} token A token to report.
267 * @param {RegExp} [pattern] Optional. A pattern of the previous
271 function checkSpacingBefore(token
, pattern
) {
272 checkMethodMap
[token
.value
].before(token
, pattern
|| PREV_TOKEN
);
276 * Reports a given token if usage of spacing preceded by the token is
278 * @param {Token} token A token to report.
279 * @param {RegExp} [pattern] Optional. A pattern of the next
283 function checkSpacingAfter(token
, pattern
) {
284 checkMethodMap
[token
.value
].after(token
, pattern
|| NEXT_TOKEN
);
288 * Reports a given token if usage of spacing around the token is invalid.
289 * @param {Token} token A token to report.
292 function checkSpacingAround(token
) {
293 checkSpacingBefore(token
);
294 checkSpacingAfter(token
);
298 * Reports the first token of a given node if the first token is a keyword
299 * and usage of spacing around the token is invalid.
300 * @param {ASTNode|null} node A node to report.
303 function checkSpacingAroundFirstToken(node
) {
304 const firstToken
= node
&& sourceCode
.getFirstToken(node
);
306 if (firstToken
&& firstToken
.type
=== "Keyword") {
307 checkSpacingAround(firstToken
);
312 * Reports the first token of a given node if the first token is a keyword
313 * and usage of spacing followed by the token is invalid.
315 * This is used for unary operators (e.g. `typeof`), `function`, and `super`.
316 * Other rules are handling usage of spacing preceded by those keywords.
317 * @param {ASTNode|null} node A node to report.
320 function checkSpacingBeforeFirstToken(node
) {
321 const firstToken
= node
&& sourceCode
.getFirstToken(node
);
323 if (firstToken
&& firstToken
.type
=== "Keyword") {
324 checkSpacingBefore(firstToken
);
329 * Reports the previous token of a given node if the token is a keyword and
330 * usage of spacing around the token is invalid.
331 * @param {ASTNode|null} node A node to report.
334 function checkSpacingAroundTokenBefore(node
) {
336 const token
= sourceCode
.getTokenBefore(node
, astUtils
.isKeywordToken
);
338 checkSpacingAround(token
);
343 * Reports `async` or `function` keywords of a given node if usage of
344 * spacing around those keywords is invalid.
345 * @param {ASTNode} node A node to report.
348 function checkSpacingForFunction(node
) {
349 const firstToken
= node
&& sourceCode
.getFirstToken(node
);
352 ((firstToken
.type
=== "Keyword" && firstToken
.value
=== "function") ||
353 firstToken
.value
=== "async")
355 checkSpacingBefore(firstToken
);
360 * Reports `class` and `extends` keywords of a given node if usage of
361 * spacing around those keywords is invalid.
362 * @param {ASTNode} node A node to report.
365 function checkSpacingForClass(node
) {
366 checkSpacingAroundFirstToken(node
);
367 checkSpacingAroundTokenBefore(node
.superClass
);
371 * Reports `if` and `else` keywords of a given node if usage of spacing
372 * around those keywords is invalid.
373 * @param {ASTNode} node A node to report.
376 function checkSpacingForIfStatement(node
) {
377 checkSpacingAroundFirstToken(node
);
378 checkSpacingAroundTokenBefore(node
.alternate
);
382 * Reports `try`, `catch`, and `finally` keywords of a given node if usage
383 * of spacing around those keywords is invalid.
384 * @param {ASTNode} node A node to report.
387 function checkSpacingForTryStatement(node
) {
388 checkSpacingAroundFirstToken(node
);
389 checkSpacingAroundFirstToken(node
.handler
);
390 checkSpacingAroundTokenBefore(node
.finalizer
);
394 * Reports `do` and `while` keywords of a given node if usage of spacing
395 * around those keywords is invalid.
396 * @param {ASTNode} node A node to report.
399 function checkSpacingForDoWhileStatement(node
) {
400 checkSpacingAroundFirstToken(node
);
401 checkSpacingAroundTokenBefore(node
.test
);
405 * Reports `for` and `in` keywords of a given node if usage of spacing
406 * around those keywords is invalid.
407 * @param {ASTNode} node A node to report.
410 function checkSpacingForForInStatement(node
) {
411 checkSpacingAroundFirstToken(node
);
413 const inToken
= sourceCode
.getTokenBefore(node
.right
, astUtils
.isNotOpeningParenToken
);
414 const previousToken
= sourceCode
.getTokenBefore(inToken
);
416 if (previousToken
.type
!== "PrivateIdentifier") {
417 checkSpacingBefore(inToken
);
420 checkSpacingAfter(inToken
);
424 * Reports `for` and `of` keywords of a given node if usage of spacing
425 * around those keywords is invalid.
426 * @param {ASTNode} node A node to report.
429 function checkSpacingForForOfStatement(node
) {
431 checkSpacingBefore(sourceCode
.getFirstToken(node
, 0));
432 checkSpacingAfter(sourceCode
.getFirstToken(node
, 1));
434 checkSpacingAroundFirstToken(node
);
437 const ofToken
= sourceCode
.getTokenBefore(node
.right
, astUtils
.isNotOpeningParenToken
);
438 const previousToken
= sourceCode
.getTokenBefore(ofToken
);
440 if (previousToken
.type
!== "PrivateIdentifier") {
441 checkSpacingBefore(ofToken
);
444 checkSpacingAfter(ofToken
);
448 * Reports `import`, `export`, `as`, and `from` keywords of a given node if
449 * usage of spacing around those keywords is invalid.
451 * This rule handles the `*` token in module declarations.
453 * import*as A from "./a"; /*error Expected space(s) after "import".
454 * error Expected space(s) before "as".
455 * @param {ASTNode} node A node to report.
458 function checkSpacingForModuleDeclaration(node
) {
459 const firstToken
= sourceCode
.getFirstToken(node
);
461 checkSpacingBefore(firstToken
, PREV_TOKEN_M
);
462 checkSpacingAfter(firstToken
, NEXT_TOKEN_M
);
464 if (node
.type
=== "ExportDefaultDeclaration") {
465 checkSpacingAround(sourceCode
.getTokenAfter(firstToken
));
468 if (node
.type
=== "ExportAllDeclaration" && node
.exported
) {
469 const asToken
= sourceCode
.getTokenBefore(node
.exported
);
471 checkSpacingBefore(asToken
, PREV_TOKEN_M
);
472 checkSpacingAfter(asToken
, NEXT_TOKEN_M
);
476 const fromToken
= sourceCode
.getTokenBefore(node
.source
);
478 checkSpacingBefore(fromToken
, PREV_TOKEN_M
);
479 checkSpacingAfter(fromToken
, NEXT_TOKEN_M
);
484 * Reports `as` keyword of a given node if usage of spacing around this
485 * keyword is invalid.
486 * @param {ASTNode} node An `ImportSpecifier` node to check.
489 function checkSpacingForImportSpecifier(node
) {
490 if (node
.imported
.range
[0] !== node
.local
.range
[0]) {
491 const asToken
= sourceCode
.getTokenBefore(node
.local
);
493 checkSpacingBefore(asToken
, PREV_TOKEN_M
);
498 * Reports `as` keyword of a given node if usage of spacing around this
499 * keyword is invalid.
500 * @param {ASTNode} node An `ExportSpecifier` node to check.
503 function checkSpacingForExportSpecifier(node
) {
504 if (node
.local
.range
[0] !== node
.exported
.range
[0]) {
505 const asToken
= sourceCode
.getTokenBefore(node
.exported
);
507 checkSpacingBefore(asToken
, PREV_TOKEN_M
);
508 checkSpacingAfter(asToken
, NEXT_TOKEN_M
);
513 * Reports `as` keyword of a given node if usage of spacing around this
514 * keyword is invalid.
515 * @param {ASTNode} node A node to report.
518 function checkSpacingForImportNamespaceSpecifier(node
) {
519 const asToken
= sourceCode
.getFirstToken(node
, 1);
521 checkSpacingBefore(asToken
, PREV_TOKEN_M
);
525 * Reports `static`, `get`, and `set` keywords of a given node if usage of
526 * spacing around those keywords is invalid.
527 * @param {ASTNode} node A node to report.
528 * @throws {Error} If unable to find token get, set, or async beside method name.
531 function checkSpacingForProperty(node
) {
533 checkSpacingAroundFirstToken(node
);
535 if (node
.kind
=== "get" ||
536 node
.kind
=== "set" ||
538 (node
.method
|| node
.type
=== "MethodDefinition") &&
542 const token
= sourceCode
.getTokenBefore(
557 throw new Error("Failed to find token get, set, or async beside method name");
561 checkSpacingAround(token
);
566 * Reports `await` keyword of a given node if usage of spacing before
567 * this keyword is invalid.
568 * @param {ASTNode} node A node to report.
571 function checkSpacingForAwaitExpression(node
) {
572 checkSpacingBefore(sourceCode
.getFirstToken(node
));
578 DebuggerStatement
: checkSpacingAroundFirstToken
,
579 WithStatement
: checkSpacingAroundFirstToken
,
581 // Statements - Control flow
582 BreakStatement
: checkSpacingAroundFirstToken
,
583 ContinueStatement
: checkSpacingAroundFirstToken
,
584 ReturnStatement
: checkSpacingAroundFirstToken
,
585 ThrowStatement
: checkSpacingAroundFirstToken
,
586 TryStatement
: checkSpacingForTryStatement
,
588 // Statements - Choice
589 IfStatement
: checkSpacingForIfStatement
,
590 SwitchStatement
: checkSpacingAroundFirstToken
,
591 SwitchCase
: checkSpacingAroundFirstToken
,
593 // Statements - Loops
594 DoWhileStatement
: checkSpacingForDoWhileStatement
,
595 ForInStatement
: checkSpacingForForInStatement
,
596 ForOfStatement
: checkSpacingForForOfStatement
,
597 ForStatement
: checkSpacingAroundFirstToken
,
598 WhileStatement
: checkSpacingAroundFirstToken
,
600 // Statements - Declarations
601 ClassDeclaration
: checkSpacingForClass
,
602 ExportNamedDeclaration
: checkSpacingForModuleDeclaration
,
603 ExportDefaultDeclaration
: checkSpacingForModuleDeclaration
,
604 ExportAllDeclaration
: checkSpacingForModuleDeclaration
,
605 FunctionDeclaration
: checkSpacingForFunction
,
606 ImportDeclaration
: checkSpacingForModuleDeclaration
,
607 VariableDeclaration
: checkSpacingAroundFirstToken
,
610 ArrowFunctionExpression
: checkSpacingForFunction
,
611 AwaitExpression
: checkSpacingForAwaitExpression
,
612 ClassExpression
: checkSpacingForClass
,
613 FunctionExpression
: checkSpacingForFunction
,
614 NewExpression
: checkSpacingBeforeFirstToken
,
615 Super
: checkSpacingBeforeFirstToken
,
616 ThisExpression
: checkSpacingBeforeFirstToken
,
617 UnaryExpression
: checkSpacingBeforeFirstToken
,
618 YieldExpression
: checkSpacingBeforeFirstToken
,
621 ImportSpecifier
: checkSpacingForImportSpecifier
,
622 ExportSpecifier
: checkSpacingForExportSpecifier
,
623 ImportNamespaceSpecifier
: checkSpacingForImportNamespaceSpecifier
,
624 MethodDefinition
: checkSpacingForProperty
,
625 PropertyDefinition
: checkSpacingForProperty
,
626 StaticBlock
: checkSpacingAroundFirstToken
,
627 Property
: checkSpacingForProperty
,
629 // To avoid conflicts with `space-infix-ops`, e.g. `a > this.b`
630 "BinaryExpression[operator='>']"(node
) {
631 const operatorToken
= sourceCode
.getTokenBefore(node
.right
, astUtils
.isNotOpeningParenToken
);
633 tokensToIgnore
.add(operatorToken
);