1/** 2 * @fileoverview Object to handle access and retrieval of tokens. 3 * @author Brandon Mills 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Requirements 9//------------------------------------------------------------------------------ 10 11const assert = require("assert"); 12const { isCommentToken } = require("eslint-utils"); 13const cursors = require("./cursors"); 14const ForwardTokenCursor = require("./forward-token-cursor"); 15const PaddedTokenCursor = require("./padded-token-cursor"); 16const utils = require("./utils"); 17 18//------------------------------------------------------------------------------ 19// Helpers 20//------------------------------------------------------------------------------ 21 22const TOKENS = Symbol("tokens"); 23const COMMENTS = Symbol("comments"); 24const INDEX_MAP = Symbol("indexMap"); 25 26/** 27 * Creates the map from locations to indices in `tokens`. 28 * 29 * The first/last location of tokens is mapped to the index of the token. 30 * The first/last location of comments is mapped to the index of the next token of each comment. 31 * @param {Token[]} tokens The array of tokens. 32 * @param {Comment[]} comments The array of comments. 33 * @returns {Object} The map from locations to indices in `tokens`. 34 * @private 35 */ 36function createIndexMap(tokens, comments) { 37 const map = Object.create(null); 38 let tokenIndex = 0; 39 let commentIndex = 0; 40 let nextStart = 0; 41 let range = null; 42 43 while (tokenIndex < tokens.length || commentIndex < comments.length) { 44 nextStart = (commentIndex < comments.length) ? comments[commentIndex].range[0] : Number.MAX_SAFE_INTEGER; 45 while (tokenIndex < tokens.length && (range = tokens[tokenIndex].range)[0] < nextStart) { 46 map[range[0]] = tokenIndex; 47 map[range[1] - 1] = tokenIndex; 48 tokenIndex += 1; 49 } 50 51 nextStart = (tokenIndex < tokens.length) ? tokens[tokenIndex].range[0] : Number.MAX_SAFE_INTEGER; 52 while (commentIndex < comments.length && (range = comments[commentIndex].range)[0] < nextStart) { 53 map[range[0]] = tokenIndex; 54 map[range[1] - 1] = tokenIndex; 55 commentIndex += 1; 56 } 57 } 58 59 return map; 60} 61 62/** 63 * Creates the cursor iterates tokens with options. 64 * @param {CursorFactory} factory The cursor factory to initialize cursor. 65 * @param {Token[]} tokens The array of tokens. 66 * @param {Comment[]} comments The array of comments. 67 * @param {Object} indexMap The map from locations to indices in `tokens`. 68 * @param {number} startLoc The start location of the iteration range. 69 * @param {number} endLoc The end location of the iteration range. 70 * @param {number|Function|Object} [opts=0] The option object. If this is a number then it's `opts.skip`. If this is a function then it's `opts.filter`. 71 * @param {boolean} [opts.includeComments=false] The flag to iterate comments as well. 72 * @param {Function|null} [opts.filter=null] The predicate function to choose tokens. 73 * @param {number} [opts.skip=0] The count of tokens the cursor skips. 74 * @returns {Cursor} The created cursor. 75 * @private 76 */ 77function createCursorWithSkip(factory, tokens, comments, indexMap, startLoc, endLoc, opts) { 78 let includeComments = false; 79 let skip = 0; 80 let filter = null; 81 82 if (typeof opts === "number") { 83 skip = opts | 0; 84 } else if (typeof opts === "function") { 85 filter = opts; 86 } else if (opts) { 87 includeComments = !!opts.includeComments; 88 skip = opts.skip | 0; 89 filter = opts.filter || null; 90 } 91 assert(skip >= 0, "options.skip should be zero or a positive integer."); 92 assert(!filter || typeof filter === "function", "options.filter should be a function."); 93 94 return factory.createCursor(tokens, comments, indexMap, startLoc, endLoc, includeComments, filter, skip, -1); 95} 96 97/** 98 * Creates the cursor iterates tokens with options. 99 * @param {CursorFactory} factory The cursor factory to initialize cursor. 100 * @param {Token[]} tokens The array of tokens. 101 * @param {Comment[]} comments The array of comments. 102 * @param {Object} indexMap The map from locations to indices in `tokens`. 103 * @param {number} startLoc The start location of the iteration range. 104 * @param {number} endLoc The end location of the iteration range. 105 * @param {number|Function|Object} [opts=0] The option object. If this is a number then it's `opts.count`. If this is a function then it's `opts.filter`. 106 * @param {boolean} [opts.includeComments] The flag to iterate comments as well. 107 * @param {Function|null} [opts.filter=null] The predicate function to choose tokens. 108 * @param {number} [opts.count=0] The maximum count of tokens the cursor iterates. Zero is no iteration for backward compatibility. 109 * @returns {Cursor} The created cursor. 110 * @private 111 */ 112function createCursorWithCount(factory, tokens, comments, indexMap, startLoc, endLoc, opts) { 113 let includeComments = false; 114 let count = 0; 115 let countExists = false; 116 let filter = null; 117 118 if (typeof opts === "number") { 119 count = opts | 0; 120 countExists = true; 121 } else if (typeof opts === "function") { 122 filter = opts; 123 } else if (opts) { 124 includeComments = !!opts.includeComments; 125 count = opts.count | 0; 126 countExists = typeof opts.count === "number"; 127 filter = opts.filter || null; 128 } 129 assert(count >= 0, "options.count should be zero or a positive integer."); 130 assert(!filter || typeof filter === "function", "options.filter should be a function."); 131 132 return factory.createCursor(tokens, comments, indexMap, startLoc, endLoc, includeComments, filter, 0, countExists ? count : -1); 133} 134 135/** 136 * Creates the cursor iterates tokens with options. 137 * This is overload function of the below. 138 * @param {Token[]} tokens The array of tokens. 139 * @param {Comment[]} comments The array of comments. 140 * @param {Object} indexMap The map from locations to indices in `tokens`. 141 * @param {number} startLoc The start location of the iteration range. 142 * @param {number} endLoc The end location of the iteration range. 143 * @param {Function|Object} opts The option object. If this is a function then it's `opts.filter`. 144 * @param {boolean} [opts.includeComments] The flag to iterate comments as well. 145 * @param {Function|null} [opts.filter=null] The predicate function to choose tokens. 146 * @param {number} [opts.count=0] The maximum count of tokens the cursor iterates. Zero is no iteration for backward compatibility. 147 * @returns {Cursor} The created cursor. 148 * @private 149 */ 150/** 151 * Creates the cursor iterates tokens with options. 152 * @param {Token[]} tokens The array of tokens. 153 * @param {Comment[]} comments The array of comments. 154 * @param {Object} indexMap The map from locations to indices in `tokens`. 155 * @param {number} startLoc The start location of the iteration range. 156 * @param {number} endLoc The end location of the iteration range. 157 * @param {number} [beforeCount=0] The number of tokens before the node to retrieve. 158 * @param {boolean} [afterCount=0] The number of tokens after the node to retrieve. 159 * @returns {Cursor} The created cursor. 160 * @private 161 */ 162function createCursorWithPadding(tokens, comments, indexMap, startLoc, endLoc, beforeCount, afterCount) { 163 if (typeof beforeCount === "undefined" && typeof afterCount === "undefined") { 164 return new ForwardTokenCursor(tokens, comments, indexMap, startLoc, endLoc); 165 } 166 if (typeof beforeCount === "number" || typeof beforeCount === "undefined") { 167 return new PaddedTokenCursor(tokens, comments, indexMap, startLoc, endLoc, beforeCount | 0, afterCount | 0); 168 } 169 return createCursorWithCount(cursors.forward, tokens, comments, indexMap, startLoc, endLoc, beforeCount); 170} 171 172/** 173 * Gets comment tokens that are adjacent to the current cursor position. 174 * @param {Cursor} cursor A cursor instance. 175 * @returns {Array} An array of comment tokens adjacent to the current cursor position. 176 * @private 177 */ 178function getAdjacentCommentTokensFromCursor(cursor) { 179 const tokens = []; 180 let currentToken = cursor.getOneToken(); 181 182 while (currentToken && isCommentToken(currentToken)) { 183 tokens.push(currentToken); 184 currentToken = cursor.getOneToken(); 185 } 186 187 return tokens; 188} 189 190//------------------------------------------------------------------------------ 191// Exports 192//------------------------------------------------------------------------------ 193 194/** 195 * The token store. 196 * 197 * This class provides methods to get tokens by locations as fast as possible. 198 * The methods are a part of public API, so we should be careful if it changes this class. 199 * 200 * People can get tokens in O(1) by the hash map which is mapping from the location of tokens/comments to tokens. 201 * Also people can get a mix of tokens and comments in O(log k), the k is the number of comments. 202 * Assuming that comments to be much fewer than tokens, this does not make hash map from token's locations to comments to reduce memory cost. 203 * This uses binary-searching instead for comments. 204 */ 205module.exports = class TokenStore { 206 207 /** 208 * Initializes this token store. 209 * @param {Token[]} tokens The array of tokens. 210 * @param {Comment[]} comments The array of comments. 211 */ 212 constructor(tokens, comments) { 213 this[TOKENS] = tokens; 214 this[COMMENTS] = comments; 215 this[INDEX_MAP] = createIndexMap(tokens, comments); 216 } 217 218 //-------------------------------------------------------------------------- 219 // Gets single token. 220 //-------------------------------------------------------------------------- 221 222 /** 223 * Gets the token starting at the specified index. 224 * @param {number} offset Index of the start of the token's range. 225 * @param {Object} [options=0] The option object. 226 * @param {boolean} [options.includeComments=false] The flag to iterate comments as well. 227 * @returns {Token|null} The token starting at index, or null if no such token. 228 */ 229 getTokenByRangeStart(offset, options) { 230 const includeComments = options && options.includeComments; 231 const token = cursors.forward.createBaseCursor( 232 this[TOKENS], 233 this[COMMENTS], 234 this[INDEX_MAP], 235 offset, 236 -1, 237 includeComments 238 ).getOneToken(); 239 240 if (token && token.range[0] === offset) { 241 return token; 242 } 243 return null; 244 } 245 246 /** 247 * Gets the first token of the given node. 248 * @param {ASTNode} node The AST node. 249 * @param {number|Function|Object} [options=0] The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`. 250 * @param {boolean} [options.includeComments=false] The flag to iterate comments as well. 251 * @param {Function|null} [options.filter=null] The predicate function to choose tokens. 252 * @param {number} [options.skip=0] The count of tokens the cursor skips. 253 * @returns {Token|null} An object representing the token. 254 */ 255 getFirstToken(node, options) { 256 return createCursorWithSkip( 257 cursors.forward, 258 this[TOKENS], 259 this[COMMENTS], 260 this[INDEX_MAP], 261 node.range[0], 262 node.range[1], 263 options 264 ).getOneToken(); 265 } 266 267 /** 268 * Gets the last token of the given node. 269 * @param {ASTNode} node The AST node. 270 * @param {number|Function|Object} [options=0] The option object. Same options as getFirstToken() 271 * @returns {Token|null} An object representing the token. 272 */ 273 getLastToken(node, options) { 274 return createCursorWithSkip( 275 cursors.backward, 276 this[TOKENS], 277 this[COMMENTS], 278 this[INDEX_MAP], 279 node.range[0], 280 node.range[1], 281 options 282 ).getOneToken(); 283 } 284 285 /** 286 * Gets the token that precedes a given node or token. 287 * @param {ASTNode|Token|Comment} node The AST node or token. 288 * @param {number|Function|Object} [options=0] The option object. Same options as getFirstToken() 289 * @returns {Token|null} An object representing the token. 290 */ 291 getTokenBefore(node, options) { 292 return createCursorWithSkip( 293 cursors.backward, 294 this[TOKENS], 295 this[COMMENTS], 296 this[INDEX_MAP], 297 -1, 298 node.range[0], 299 options 300 ).getOneToken(); 301 } 302 303 /** 304 * Gets the token that follows a given node or token. 305 * @param {ASTNode|Token|Comment} node The AST node or token. 306 * @param {number|Function|Object} [options=0] The option object. Same options as getFirstToken() 307 * @returns {Token|null} An object representing the token. 308 */ 309 getTokenAfter(node, options) { 310 return createCursorWithSkip( 311 cursors.forward, 312 this[TOKENS], 313 this[COMMENTS], 314 this[INDEX_MAP], 315 node.range[1], 316 -1, 317 options 318 ).getOneToken(); 319 } 320 321 /** 322 * Gets the first token between two non-overlapping nodes. 323 * @param {ASTNode|Token|Comment} left Node before the desired token range. 324 * @param {ASTNode|Token|Comment} right Node after the desired token range. 325 * @param {number|Function|Object} [options=0] The option object. Same options as getFirstToken() 326 * @returns {Token|null} An object representing the token. 327 */ 328 getFirstTokenBetween(left, right, options) { 329 return createCursorWithSkip( 330 cursors.forward, 331 this[TOKENS], 332 this[COMMENTS], 333 this[INDEX_MAP], 334 left.range[1], 335 right.range[0], 336 options 337 ).getOneToken(); 338 } 339 340 /** 341 * Gets the last token between two non-overlapping nodes. 342 * @param {ASTNode|Token|Comment} left Node before the desired token range. 343 * @param {ASTNode|Token|Comment} right Node after the desired token range. 344 * @param {number|Function|Object} [options=0] The option object. Same options as getFirstToken() 345 * @returns {Token|null} An object representing the token. 346 */ 347 getLastTokenBetween(left, right, options) { 348 return createCursorWithSkip( 349 cursors.backward, 350 this[TOKENS], 351 this[COMMENTS], 352 this[INDEX_MAP], 353 left.range[1], 354 right.range[0], 355 options 356 ).getOneToken(); 357 } 358 359 /** 360 * Gets the token that precedes a given node or token in the token stream. 361 * This is defined for backward compatibility. Use `includeComments` option instead. 362 * TODO: We have a plan to remove this in a future major version. 363 * @param {ASTNode|Token|Comment} node The AST node or token. 364 * @param {number} [skip=0] A number of tokens to skip. 365 * @returns {Token|null} An object representing the token. 366 * @deprecated 367 */ 368 getTokenOrCommentBefore(node, skip) { 369 return this.getTokenBefore(node, { includeComments: true, skip }); 370 } 371 372 /** 373 * Gets the token that follows a given node or token in the token stream. 374 * This is defined for backward compatibility. Use `includeComments` option instead. 375 * TODO: We have a plan to remove this in a future major version. 376 * @param {ASTNode|Token|Comment} node The AST node or token. 377 * @param {number} [skip=0] A number of tokens to skip. 378 * @returns {Token|null} An object representing the token. 379 * @deprecated 380 */ 381 getTokenOrCommentAfter(node, skip) { 382 return this.getTokenAfter(node, { includeComments: true, skip }); 383 } 384 385 //-------------------------------------------------------------------------- 386 // Gets multiple tokens. 387 //-------------------------------------------------------------------------- 388 389 /** 390 * Gets the first `count` tokens of the given node. 391 * @param {ASTNode} node The AST node. 392 * @param {number|Function|Object} [options=0] The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`. 393 * @param {boolean} [options.includeComments=false] The flag to iterate comments as well. 394 * @param {Function|null} [options.filter=null] The predicate function to choose tokens. 395 * @param {number} [options.count=0] The maximum count of tokens the cursor iterates. 396 * @returns {Token[]} Tokens. 397 */ 398 getFirstTokens(node, options) { 399 return createCursorWithCount( 400 cursors.forward, 401 this[TOKENS], 402 this[COMMENTS], 403 this[INDEX_MAP], 404 node.range[0], 405 node.range[1], 406 options 407 ).getAllTokens(); 408 } 409 410 /** 411 * Gets the last `count` tokens of the given node. 412 * @param {ASTNode} node The AST node. 413 * @param {number|Function|Object} [options=0] The option object. Same options as getFirstTokens() 414 * @returns {Token[]} Tokens. 415 */ 416 getLastTokens(node, options) { 417 return createCursorWithCount( 418 cursors.backward, 419 this[TOKENS], 420 this[COMMENTS], 421 this[INDEX_MAP], 422 node.range[0], 423 node.range[1], 424 options 425 ).getAllTokens().reverse(); 426 } 427 428 /** 429 * Gets the `count` tokens that precedes a given node or token. 430 * @param {ASTNode|Token|Comment} node The AST node or token. 431 * @param {number|Function|Object} [options=0] The option object. Same options as getFirstTokens() 432 * @returns {Token[]} Tokens. 433 */ 434 getTokensBefore(node, options) { 435 return createCursorWithCount( 436 cursors.backward, 437 this[TOKENS], 438 this[COMMENTS], 439 this[INDEX_MAP], 440 -1, 441 node.range[0], 442 options 443 ).getAllTokens().reverse(); 444 } 445 446 /** 447 * Gets the `count` tokens that follows a given node or token. 448 * @param {ASTNode|Token|Comment} node The AST node or token. 449 * @param {number|Function|Object} [options=0] The option object. Same options as getFirstTokens() 450 * @returns {Token[]} Tokens. 451 */ 452 getTokensAfter(node, options) { 453 return createCursorWithCount( 454 cursors.forward, 455 this[TOKENS], 456 this[COMMENTS], 457 this[INDEX_MAP], 458 node.range[1], 459 -1, 460 options 461 ).getAllTokens(); 462 } 463 464 /** 465 * Gets the first `count` tokens between two non-overlapping nodes. 466 * @param {ASTNode|Token|Comment} left Node before the desired token range. 467 * @param {ASTNode|Token|Comment} right Node after the desired token range. 468 * @param {number|Function|Object} [options=0] The option object. Same options as getFirstTokens() 469 * @returns {Token[]} Tokens between left and right. 470 */ 471 getFirstTokensBetween(left, right, options) { 472 return createCursorWithCount( 473 cursors.forward, 474 this[TOKENS], 475 this[COMMENTS], 476 this[INDEX_MAP], 477 left.range[1], 478 right.range[0], 479 options 480 ).getAllTokens(); 481 } 482 483 /** 484 * Gets the last `count` tokens between two non-overlapping nodes. 485 * @param {ASTNode|Token|Comment} left Node before the desired token range. 486 * @param {ASTNode|Token|Comment} right Node after the desired token range. 487 * @param {number|Function|Object} [options=0] The option object. Same options as getFirstTokens() 488 * @returns {Token[]} Tokens between left and right. 489 */ 490 getLastTokensBetween(left, right, options) { 491 return createCursorWithCount( 492 cursors.backward, 493 this[TOKENS], 494 this[COMMENTS], 495 this[INDEX_MAP], 496 left.range[1], 497 right.range[0], 498 options 499 ).getAllTokens().reverse(); 500 } 501 502 /** 503 * Gets all tokens that are related to the given node. 504 * @param {ASTNode} node The AST node. 505 * @param {Function|Object} options The option object. If this is a function then it's `options.filter`. 506 * @param {boolean} [options.includeComments=false] The flag to iterate comments as well. 507 * @param {Function|null} [options.filter=null] The predicate function to choose tokens. 508 * @param {number} [options.count=0] The maximum count of tokens the cursor iterates. 509 * @returns {Token[]} Array of objects representing tokens. 510 */ 511 /** 512 * Gets all tokens that are related to the given node. 513 * @param {ASTNode} node The AST node. 514 * @param {int} [beforeCount=0] The number of tokens before the node to retrieve. 515 * @param {int} [afterCount=0] The number of tokens after the node to retrieve. 516 * @returns {Token[]} Array of objects representing tokens. 517 */ 518 getTokens(node, beforeCount, afterCount) { 519 return createCursorWithPadding( 520 this[TOKENS], 521 this[COMMENTS], 522 this[INDEX_MAP], 523 node.range[0], 524 node.range[1], 525 beforeCount, 526 afterCount 527 ).getAllTokens(); 528 } 529 530 /** 531 * Gets all of the tokens between two non-overlapping nodes. 532 * @param {ASTNode|Token|Comment} left Node before the desired token range. 533 * @param {ASTNode|Token|Comment} right Node after the desired token range. 534 * @param {Function|Object} options The option object. If this is a function then it's `options.filter`. 535 * @param {boolean} [options.includeComments=false] The flag to iterate comments as well. 536 * @param {Function|null} [options.filter=null] The predicate function to choose tokens. 537 * @param {number} [options.count=0] The maximum count of tokens the cursor iterates. 538 * @returns {Token[]} Tokens between left and right. 539 */ 540 /** 541 * Gets all of the tokens between two non-overlapping nodes. 542 * @param {ASTNode|Token|Comment} left Node before the desired token range. 543 * @param {ASTNode|Token|Comment} right Node after the desired token range. 544 * @param {int} [padding=0] Number of extra tokens on either side of center. 545 * @returns {Token[]} Tokens between left and right. 546 */ 547 getTokensBetween(left, right, padding) { 548 return createCursorWithPadding( 549 this[TOKENS], 550 this[COMMENTS], 551 this[INDEX_MAP], 552 left.range[1], 553 right.range[0], 554 padding, 555 padding 556 ).getAllTokens(); 557 } 558 559 //-------------------------------------------------------------------------- 560 // Others. 561 //-------------------------------------------------------------------------- 562 563 /** 564 * Checks whether any comments exist or not between the given 2 nodes. 565 * @param {ASTNode} left The node to check. 566 * @param {ASTNode} right The node to check. 567 * @returns {boolean} `true` if one or more comments exist. 568 */ 569 commentsExistBetween(left, right) { 570 const index = utils.search(this[COMMENTS], left.range[1]); 571 572 return ( 573 index < this[COMMENTS].length && 574 this[COMMENTS][index].range[1] <= right.range[0] 575 ); 576 } 577 578 /** 579 * Gets all comment tokens directly before the given node or token. 580 * @param {ASTNode|token} nodeOrToken The AST node or token to check for adjacent comment tokens. 581 * @returns {Array} An array of comments in occurrence order. 582 */ 583 getCommentsBefore(nodeOrToken) { 584 const cursor = createCursorWithCount( 585 cursors.backward, 586 this[TOKENS], 587 this[COMMENTS], 588 this[INDEX_MAP], 589 -1, 590 nodeOrToken.range[0], 591 { includeComments: true } 592 ); 593 594 return getAdjacentCommentTokensFromCursor(cursor).reverse(); 595 } 596 597 /** 598 * Gets all comment tokens directly after the given node or token. 599 * @param {ASTNode|token} nodeOrToken The AST node or token to check for adjacent comment tokens. 600 * @returns {Array} An array of comments in occurrence order. 601 */ 602 getCommentsAfter(nodeOrToken) { 603 const cursor = createCursorWithCount( 604 cursors.forward, 605 this[TOKENS], 606 this[COMMENTS], 607 this[INDEX_MAP], 608 nodeOrToken.range[1], 609 -1, 610 { includeComments: true } 611 ); 612 613 return getAdjacentCommentTokensFromCursor(cursor); 614 } 615 616 /** 617 * Gets all comment tokens inside the given node. 618 * @param {ASTNode} node The AST node to get the comments for. 619 * @returns {Array} An array of comments in occurrence order. 620 */ 621 getCommentsInside(node) { 622 return this.getTokens(node, { 623 includeComments: true, 624 filter: isCommentToken 625 }); 626 } 627}; 628