• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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