]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/keyword-spacing.js
import 8.41.0 source
[pve-eslint.git] / eslint / lib / rules / keyword-spacing.js
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
12 const astUtils = require("./utils/ast-utils"),
13 keywords = require("./utils/keywords");
14
15 //------------------------------------------------------------------------------
16 // Constants
17 //------------------------------------------------------------------------------
18
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"]);
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 * @param {Token} token A token to check.
45 * @returns {boolean} `true` if the token is a "Template" token ends with "${".
46 */
47 function isOpenParenOfTemplate(token) {
48 return token.type === "Template" && TEMPLATE_OPEN_PAREN.test(token.value);
49 }
50
51 /**
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 "}".
55 */
56 function isCloseParenOfTemplate(token) {
57 return token.type === "Template" && TEMPLATE_CLOSE_PAREN.test(token.value);
58 }
59
60 //------------------------------------------------------------------------------
61 // Rule Definition
62 //------------------------------------------------------------------------------
63
64 /** @type {import('../shared/types').Rule} */
65 module.exports = {
66 meta: {
67 type: "layout",
68
69 docs: {
70 description: "Enforce consistent spacing before and after keywords",
71 recommended: false,
72 url: "https://eslint.org/docs/latest/rules/keyword-spacing"
73 },
74
75 fixable: "whitespace",
76
77 schema: [
78 {
79 type: "object",
80 properties: {
81 before: { type: "boolean", default: true },
82 after: { type: "boolean", default: true },
83 overrides: {
84 type: "object",
85 properties: KEYS.reduce((retv, key) => {
86 retv[key] = {
87 type: "object",
88 properties: {
89 before: { type: "boolean" },
90 after: { type: "boolean" }
91 },
92 additionalProperties: false
93 };
94 return retv;
95 }, {}),
96 additionalProperties: false
97 }
98 },
99 additionalProperties: false
100 }
101 ],
102 messages: {
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}}\"."
107 }
108 },
109
110 create(context) {
111 const sourceCode = context.sourceCode;
112
113 const tokensToIgnore = new WeakSet();
114
115 /**
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.
119 * @returns {void}
120 */
121 function expectSpaceBefore(token, pattern) {
122 const prevToken = sourceCode.getTokenBefore(token);
123
124 if (prevToken &&
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)
130 ) {
131 context.report({
132 loc: token.loc,
133 messageId: "expectedBefore",
134 data: token,
135 fix(fixer) {
136 return fixer.insertTextBefore(token, " ");
137 }
138 });
139 }
140 }
141
142 /**
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.
146 * @returns {void}
147 */
148 function unexpectSpaceBefore(token, pattern) {
149 const prevToken = sourceCode.getTokenBefore(token);
150
151 if (prevToken &&
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)
157 ) {
158 context.report({
159 loc: { start: prevToken.loc.end, end: token.loc.start },
160 messageId: "unexpectedBefore",
161 data: token,
162 fix(fixer) {
163 return fixer.removeRange([prevToken.range[1], token.range[0]]);
164 }
165 });
166 }
167 }
168
169 /**
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.
173 * @returns {void}
174 */
175 function expectSpaceAfter(token, pattern) {
176 const nextToken = sourceCode.getTokenAfter(token);
177
178 if (nextToken &&
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)
184 ) {
185 context.report({
186 loc: token.loc,
187 messageId: "expectedAfter",
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 * @param {Token} token A token to report.
199 * @param {RegExp} pattern A pattern of the next token to check.
200 * @returns {void}
201 */
202 function unexpectSpaceAfter(token, pattern) {
203 const nextToken = sourceCode.getTokenAfter(token);
204
205 if (nextToken &&
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)
211 ) {
212
213 context.report({
214 loc: { start: token.loc.end, end: nextToken.loc.start },
215 messageId: "unexpectedAfter",
216 data: token,
217 fix(fixer) {
218 return fixer.removeRange([token.range[1], nextToken.range[0]]);
219 }
220 });
221 }
222 }
223
224 /**
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}`.
230 */
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
237 };
238 const overrides = (options && options.overrides) || {};
239 const retv = Object.create(null);
240
241 for (let i = 0; i < KEYS.length; ++i) {
242 const key = KEYS[i];
243 const override = overrides[key];
244
245 if (override) {
246 const thisBefore = ("before" in override) ? override.before : before;
247 const thisAfter = ("after" in override) ? override.after : after;
248
249 retv[key] = {
250 before: thisBefore ? expectSpaceBefore : unexpectSpaceBefore,
251 after: thisAfter ? expectSpaceAfter : unexpectSpaceAfter
252 };
253 } else {
254 retv[key] = defaultValue;
255 }
256 }
257
258 return retv;
259 }
260
261 const checkMethodMap = parseOptions(context.options[0]);
262
263 /**
264 * Reports a given token if usage of spacing followed by the token is
265 * invalid.
266 * @param {Token} token A token to report.
267 * @param {RegExp} [pattern] Optional. A pattern of the previous
268 * token to check.
269 * @returns {void}
270 */
271 function checkSpacingBefore(token, pattern) {
272 checkMethodMap[token.value].before(token, pattern || PREV_TOKEN);
273 }
274
275 /**
276 * Reports a given token if usage of spacing preceded by the token is
277 * invalid.
278 * @param {Token} token A token to report.
279 * @param {RegExp} [pattern] Optional. A pattern of the next
280 * token to check.
281 * @returns {void}
282 */
283 function checkSpacingAfter(token, pattern) {
284 checkMethodMap[token.value].after(token, pattern || NEXT_TOKEN);
285 }
286
287 /**
288 * Reports a given token if usage of spacing around the token is invalid.
289 * @param {Token} token A token to report.
290 * @returns {void}
291 */
292 function checkSpacingAround(token) {
293 checkSpacingBefore(token);
294 checkSpacingAfter(token);
295 }
296
297 /**
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.
301 * @returns {void}
302 */
303 function checkSpacingAroundFirstToken(node) {
304 const firstToken = node && sourceCode.getFirstToken(node);
305
306 if (firstToken && firstToken.type === "Keyword") {
307 checkSpacingAround(firstToken);
308 }
309 }
310
311 /**
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.
314 *
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.
318 * @returns {void}
319 */
320 function checkSpacingBeforeFirstToken(node) {
321 const firstToken = node && sourceCode.getFirstToken(node);
322
323 if (firstToken && firstToken.type === "Keyword") {
324 checkSpacingBefore(firstToken);
325 }
326 }
327
328 /**
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.
332 * @returns {void}
333 */
334 function checkSpacingAroundTokenBefore(node) {
335 if (node) {
336 const token = sourceCode.getTokenBefore(node, astUtils.isKeywordToken);
337
338 checkSpacingAround(token);
339 }
340 }
341
342 /**
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.
346 * @returns {void}
347 */
348 function checkSpacingForFunction(node) {
349 const firstToken = node && sourceCode.getFirstToken(node);
350
351 if (firstToken &&
352 ((firstToken.type === "Keyword" && firstToken.value === "function") ||
353 firstToken.value === "async")
354 ) {
355 checkSpacingBefore(firstToken);
356 }
357 }
358
359 /**
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.
363 * @returns {void}
364 */
365 function checkSpacingForClass(node) {
366 checkSpacingAroundFirstToken(node);
367 checkSpacingAroundTokenBefore(node.superClass);
368 }
369
370 /**
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.
374 * @returns {void}
375 */
376 function checkSpacingForIfStatement(node) {
377 checkSpacingAroundFirstToken(node);
378 checkSpacingAroundTokenBefore(node.alternate);
379 }
380
381 /**
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.
385 * @returns {void}
386 */
387 function checkSpacingForTryStatement(node) {
388 checkSpacingAroundFirstToken(node);
389 checkSpacingAroundFirstToken(node.handler);
390 checkSpacingAroundTokenBefore(node.finalizer);
391 }
392
393 /**
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.
397 * @returns {void}
398 */
399 function checkSpacingForDoWhileStatement(node) {
400 checkSpacingAroundFirstToken(node);
401 checkSpacingAroundTokenBefore(node.test);
402 }
403
404 /**
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.
408 * @returns {void}
409 */
410 function checkSpacingForForInStatement(node) {
411 checkSpacingAroundFirstToken(node);
412
413 const inToken = sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken);
414 const previousToken = sourceCode.getTokenBefore(inToken);
415
416 if (previousToken.type !== "PrivateIdentifier") {
417 checkSpacingBefore(inToken);
418 }
419
420 checkSpacingAfter(inToken);
421 }
422
423 /**
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.
427 * @returns {void}
428 */
429 function checkSpacingForForOfStatement(node) {
430 if (node.await) {
431 checkSpacingBefore(sourceCode.getFirstToken(node, 0));
432 checkSpacingAfter(sourceCode.getFirstToken(node, 1));
433 } else {
434 checkSpacingAroundFirstToken(node);
435 }
436
437 const ofToken = sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken);
438 const previousToken = sourceCode.getTokenBefore(ofToken);
439
440 if (previousToken.type !== "PrivateIdentifier") {
441 checkSpacingBefore(ofToken);
442 }
443
444 checkSpacingAfter(ofToken);
445 }
446
447 /**
448 * Reports `import`, `export`, `as`, and `from` keywords of a given node if
449 * usage of spacing around those keywords is invalid.
450 *
451 * This rule handles the `*` token in module declarations.
452 *
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.
456 * @returns {void}
457 */
458 function checkSpacingForModuleDeclaration(node) {
459 const firstToken = sourceCode.getFirstToken(node);
460
461 checkSpacingBefore(firstToken, PREV_TOKEN_M);
462 checkSpacingAfter(firstToken, NEXT_TOKEN_M);
463
464 if (node.type === "ExportDefaultDeclaration") {
465 checkSpacingAround(sourceCode.getTokenAfter(firstToken));
466 }
467
468 if (node.type === "ExportAllDeclaration" && node.exported) {
469 const asToken = sourceCode.getTokenBefore(node.exported);
470
471 checkSpacingBefore(asToken, PREV_TOKEN_M);
472 checkSpacingAfter(asToken, NEXT_TOKEN_M);
473 }
474
475 if (node.source) {
476 const fromToken = sourceCode.getTokenBefore(node.source);
477
478 checkSpacingBefore(fromToken, PREV_TOKEN_M);
479 checkSpacingAfter(fromToken, NEXT_TOKEN_M);
480 }
481 }
482
483 /**
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.
487 * @returns {void}
488 */
489 function checkSpacingForImportSpecifier(node) {
490 if (node.imported.range[0] !== node.local.range[0]) {
491 const asToken = sourceCode.getTokenBefore(node.local);
492
493 checkSpacingBefore(asToken, PREV_TOKEN_M);
494 }
495 }
496
497 /**
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.
501 * @returns {void}
502 */
503 function checkSpacingForExportSpecifier(node) {
504 if (node.local.range[0] !== node.exported.range[0]) {
505 const asToken = sourceCode.getTokenBefore(node.exported);
506
507 checkSpacingBefore(asToken, PREV_TOKEN_M);
508 checkSpacingAfter(asToken, NEXT_TOKEN_M);
509 }
510 }
511
512 /**
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.
516 * @returns {void}
517 */
518 function checkSpacingForImportNamespaceSpecifier(node) {
519 const asToken = sourceCode.getFirstToken(node, 1);
520
521 checkSpacingBefore(asToken, PREV_TOKEN_M);
522 }
523
524 /**
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.
529 * @returns {void}
530 */
531 function checkSpacingForProperty(node) {
532 if (node.static) {
533 checkSpacingAroundFirstToken(node);
534 }
535 if (node.kind === "get" ||
536 node.kind === "set" ||
537 (
538 (node.method || node.type === "MethodDefinition") &&
539 node.value.async
540 )
541 ) {
542 const token = sourceCode.getTokenBefore(
543 node.key,
544 tok => {
545 switch (tok.value) {
546 case "get":
547 case "set":
548 case "async":
549 return true;
550 default:
551 return false;
552 }
553 }
554 );
555
556 if (!token) {
557 throw new Error("Failed to find token get, set, or async beside method name");
558 }
559
560
561 checkSpacingAround(token);
562 }
563 }
564
565 /**
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.
569 * @returns {void}
570 */
571 function checkSpacingForAwaitExpression(node) {
572 checkSpacingBefore(sourceCode.getFirstToken(node));
573 }
574
575 return {
576
577 // Statements
578 DebuggerStatement: checkSpacingAroundFirstToken,
579 WithStatement: checkSpacingAroundFirstToken,
580
581 // Statements - Control flow
582 BreakStatement: checkSpacingAroundFirstToken,
583 ContinueStatement: checkSpacingAroundFirstToken,
584 ReturnStatement: checkSpacingAroundFirstToken,
585 ThrowStatement: checkSpacingAroundFirstToken,
586 TryStatement: checkSpacingForTryStatement,
587
588 // Statements - Choice
589 IfStatement: checkSpacingForIfStatement,
590 SwitchStatement: checkSpacingAroundFirstToken,
591 SwitchCase: checkSpacingAroundFirstToken,
592
593 // Statements - Loops
594 DoWhileStatement: checkSpacingForDoWhileStatement,
595 ForInStatement: checkSpacingForForInStatement,
596 ForOfStatement: checkSpacingForForOfStatement,
597 ForStatement: checkSpacingAroundFirstToken,
598 WhileStatement: checkSpacingAroundFirstToken,
599
600 // Statements - Declarations
601 ClassDeclaration: checkSpacingForClass,
602 ExportNamedDeclaration: checkSpacingForModuleDeclaration,
603 ExportDefaultDeclaration: checkSpacingForModuleDeclaration,
604 ExportAllDeclaration: checkSpacingForModuleDeclaration,
605 FunctionDeclaration: checkSpacingForFunction,
606 ImportDeclaration: checkSpacingForModuleDeclaration,
607 VariableDeclaration: checkSpacingAroundFirstToken,
608
609 // Expressions
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,
619
620 // Others
621 ImportSpecifier: checkSpacingForImportSpecifier,
622 ExportSpecifier: checkSpacingForExportSpecifier,
623 ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier,
624 MethodDefinition: checkSpacingForProperty,
625 PropertyDefinition: checkSpacingForProperty,
626 StaticBlock: checkSpacingAroundFirstToken,
627 Property: checkSpacingForProperty,
628
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);
632
633 tokensToIgnore.add(operatorToken);
634 }
635 };
636 }
637 };