UNPKG

24.7 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 { 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};