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