UNPKG

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