mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
1193c9dbaf
dogfood
1561 lines
53 KiB
TypeScript
1561 lines
53 KiB
TypeScript
/*
|
|
* `SourceCode` methods related to tokens.
|
|
*/
|
|
|
|
import { cachedTokens, tokensInt32, tokensLen, initTokensBuffer, getToken } from "./tokens.ts";
|
|
import {
|
|
tokensAndCommentsInt32,
|
|
tokensAndCommentsLen,
|
|
getTokenOrComment,
|
|
getTokenOrCommentEnd,
|
|
initTokensAndCommentsBuffer,
|
|
} from "./tokens_and_comments.ts";
|
|
import { debugAssertIsNonNull } from "../utils/asserts.ts";
|
|
|
|
import type { Node, NodeOrToken } from "./types.ts";
|
|
import type { Token } from "./tokens.ts";
|
|
import type { TokenOrComment } from "./tokens_and_comments.ts";
|
|
|
|
/**
|
|
* Options for various `SourceCode` methods e.g. `getFirstToken`.
|
|
*/
|
|
export interface SkipOptions {
|
|
/** Number of skipping tokens */
|
|
skip?: number;
|
|
/** `true` to include comment tokens in the result */
|
|
includeComments?: boolean;
|
|
/** Function to filter tokens */
|
|
filter?: FilterFn | null;
|
|
}
|
|
|
|
/**
|
|
* Options for various `SourceCode` methods e.g. `getFirstTokens`.
|
|
*/
|
|
export interface CountOptions {
|
|
/** Maximum number of tokens to return */
|
|
count?: number;
|
|
/** `true` to include comment tokens in the result */
|
|
includeComments?: boolean;
|
|
/** Function to filter tokens */
|
|
filter?: FilterFn | null;
|
|
}
|
|
|
|
/**
|
|
* Options for `getTokenByRangeStart`.
|
|
*/
|
|
export interface RangeOptions {
|
|
/** `true` to include comment tokens in the result */
|
|
includeComments?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Filter function, passed as `filter` property of `SkipOptions` and `CountOptions`.
|
|
*/
|
|
export type FilterFn = (token: TokenOrComment) => boolean;
|
|
|
|
/**
|
|
* Whether `Options` may include comment tokens in the result.
|
|
* Resolves to `true` if `Options` has an `includeComments` property whose type includes `true`
|
|
* (i.e. it's `true`, `boolean`, or `boolean | undefined`), and `false` otherwise.
|
|
*/
|
|
type MayIncludeComments<Options> = Options extends { includeComments: false }
|
|
? false
|
|
: "includeComments" extends keyof Options
|
|
? true
|
|
: false;
|
|
|
|
/**
|
|
* Resolves to `TokenOrComment` if `Options` may include comments, `Token` otherwise.
|
|
*/
|
|
type TokenResult<Options> = MayIncludeComments<Options> extends true ? TokenOrComment : Token;
|
|
|
|
// `SkipOptions` object used by `getTokenOrCommentBefore` and `getTokenOrCommentAfter`.
|
|
// This object is reused over and over to avoid creating a new options object on each call.
|
|
const INCLUDE_COMMENTS_SKIP_OPTIONS: SkipOptions = { includeComments: true, skip: 0 };
|
|
|
|
/**
|
|
* Get all tokens that are related to the given node.
|
|
* @param node - The AST node.
|
|
* @param countOptions? - Options object. If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns Array of `Token`s, or array of `Token | Comment`s if `includeComments` is `true`.
|
|
*/
|
|
/**
|
|
* Get all tokens that are related to the given node.
|
|
* @param node - The AST node.
|
|
* @param beforeCount? - The number of tokens before the node to retrieve.
|
|
* @param afterCount? - The number of tokens after the node to retrieve.
|
|
* @returns Array of `Token`s, or array of `Token | Comment`s if `includeComments` is `true`.
|
|
*/
|
|
export function getTokens<Options extends CountOptions | number | FilterFn | null | undefined>(
|
|
node: Node,
|
|
countOptions?: Options,
|
|
afterCount?: number | null,
|
|
): TokenResult<Options>[] {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options>[];
|
|
|
|
// Maximum number of tokens to return
|
|
let count = typeof countOptions === "object" && countOptions !== null ? countOptions.count : null;
|
|
|
|
// Number of preceding tokens to additionally return
|
|
const beforeCount = typeof countOptions === "number" ? countOptions : 0;
|
|
|
|
// Number of following tokens to additionally return
|
|
afterCount =
|
|
(typeof countOptions === "number" || typeof countOptions === "undefined")
|
|
&& typeof afterCount === "number"
|
|
? afterCount
|
|
: 0;
|
|
|
|
// Function to filter tokens
|
|
const filter: FilterFn | null | undefined =
|
|
typeof countOptions === "function"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(countOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
const { range } = node,
|
|
rangeStart = range[0],
|
|
rangeEnd = range[1];
|
|
|
|
// Binary search for first token past `node`'s start
|
|
let sliceStart = firstTokenAtOrAfter(int32, rangeStart, 0, len);
|
|
// Binary search for first token past `node`'s end
|
|
let sliceEnd = firstTokenAtOrAfter(int32, rangeEnd, sliceStart, len);
|
|
|
|
sliceStart = Math.max(0, sliceStart - beforeCount);
|
|
sliceEnd = Math.min(sliceEnd + afterCount, len);
|
|
|
|
if (typeof filter !== "function") {
|
|
const end = Math.min(sliceStart + (count ?? sliceEnd), sliceEnd);
|
|
return collectEntries(sliceStart, end, includeComments) as Result;
|
|
}
|
|
|
|
const allTokens: TokenOrComment[] = [];
|
|
|
|
if (typeof count !== "number") {
|
|
for (let i = sliceStart; i < sliceEnd; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) allTokens.push(token);
|
|
}
|
|
return allTokens as Result;
|
|
}
|
|
|
|
for (let i = sliceStart; i < sliceEnd && count > 0; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) {
|
|
allTokens.push(token);
|
|
count--;
|
|
}
|
|
}
|
|
|
|
return allTokens as Result;
|
|
}
|
|
|
|
/**
|
|
* Get the first token of the given node.
|
|
* @param node - The AST node.
|
|
* @param skipOptions? - Options object.
|
|
* If is a number, equivalent to `{ skip: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns `Token` (or `Token | Comment` if `includeComments` is `true`), or `null` if none found.
|
|
*/
|
|
export function getFirstToken<Options extends SkipOptions | number | FilterFn | null | undefined>(
|
|
node: Node,
|
|
skipOptions?: Options,
|
|
): TokenResult<Options> | null {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options> | null;
|
|
|
|
// Number of tokens at the beginning of the given node to skip
|
|
let skip =
|
|
typeof skipOptions === "number"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.skip
|
|
: null;
|
|
|
|
// Filter function
|
|
const filter: FilterFn | null | undefined =
|
|
typeof skipOptions === "function"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(skipOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
const { range } = node,
|
|
rangeStart = range[0],
|
|
rangeEnd = range[1];
|
|
|
|
// Binary search for first token past `node`'s start
|
|
const startIndex = firstTokenAtOrAfter(int32, rangeStart, 0, len);
|
|
|
|
if (typeof filter !== "function") {
|
|
const skipTo = startIndex + (skip ?? 0);
|
|
if (skipTo >= len) return null;
|
|
if (entryStart(skipTo, int32) >= rangeEnd) return null;
|
|
return getEntry(skipTo, includeComments) as Result;
|
|
}
|
|
|
|
if (typeof skip !== "number") {
|
|
for (let i = startIndex; i < len; i++) {
|
|
if (entryStart(i, int32) >= rangeEnd) return null;
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) return token as Result;
|
|
}
|
|
} else {
|
|
for (let i = startIndex; i < len; i++) {
|
|
if (entryStart(i, int32) >= rangeEnd) return null;
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) {
|
|
if (skip <= 0) return token as Result;
|
|
skip--;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the first tokens of the given node.
|
|
* @param node - The AST node.
|
|
* @param countOptions? - Options object.
|
|
* If is a number, equivalent to `{ count: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns Array of `Token`s, or array of `Token | Comment`s if `includeComments` is `true`.
|
|
*/
|
|
export function getFirstTokens<Options extends CountOptions | number | FilterFn | null | undefined>(
|
|
node: Node,
|
|
countOptions?: Options,
|
|
): TokenResult<Options>[] {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options>[];
|
|
|
|
const count =
|
|
typeof countOptions === "number"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.count
|
|
: null;
|
|
|
|
const filter: FilterFn | null | undefined =
|
|
typeof countOptions === "function"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(countOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
const { range } = node,
|
|
rangeStart = range[0],
|
|
rangeEnd = range[1];
|
|
|
|
// Binary search for first token past `node`'s start
|
|
const sliceStart = firstTokenAtOrAfter(int32, rangeStart, 0, len);
|
|
// Binary search for first token past `node`'s end
|
|
const sliceEnd = firstTokenAtOrAfter(int32, rangeEnd, sliceStart, len);
|
|
|
|
if (typeof filter !== "function") {
|
|
if (typeof count !== "number") {
|
|
return collectEntries(sliceStart, sliceEnd, includeComments) as Result;
|
|
}
|
|
return collectEntries(
|
|
sliceStart,
|
|
Math.min(sliceStart + count, sliceEnd),
|
|
includeComments,
|
|
) as Result;
|
|
}
|
|
|
|
const firstTokens: TokenOrComment[] = [];
|
|
if (typeof count !== "number") {
|
|
for (let i = sliceStart; i < sliceEnd; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) firstTokens.push(token);
|
|
}
|
|
} else {
|
|
for (let i = sliceStart; i < sliceEnd && firstTokens.length < count; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) firstTokens.push(token);
|
|
}
|
|
}
|
|
return firstTokens as Result;
|
|
}
|
|
|
|
/**
|
|
* Get the last token of the given node.
|
|
* @param node - The AST node.
|
|
* @param skipOptions? - Options object.
|
|
* If is a number, equivalent to `{ skip: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns `Token` (or `Token | Comment` if `includeComments` is `true`), or `null` if none found.
|
|
*/
|
|
export function getLastToken<Options extends SkipOptions | number | FilterFn | null | undefined>(
|
|
node: Node,
|
|
skipOptions?: Options,
|
|
): TokenResult<Options> | null {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options> | null;
|
|
|
|
// Number of tokens at the end of the given node to skip
|
|
let skip =
|
|
typeof skipOptions === "number"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.skip
|
|
: null;
|
|
|
|
const filter: FilterFn | null | undefined =
|
|
typeof skipOptions === "function"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(skipOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
const { range } = node,
|
|
rangeStart = range[0],
|
|
rangeEnd = range[1];
|
|
|
|
// Binary search for token immediately before `node`'s end
|
|
const lastTokenIndex = firstTokenAtOrAfter(int32, rangeEnd, 0, len) - 1;
|
|
|
|
if (typeof filter !== "function") {
|
|
const skipTo = lastTokenIndex - (skip ?? 0);
|
|
if (skipTo < 0) return null;
|
|
if (entryStart(skipTo, int32) < rangeStart) return null;
|
|
return getEntry(skipTo, includeComments) as Result;
|
|
}
|
|
|
|
if (typeof skip !== "number") {
|
|
for (let i = lastTokenIndex; i >= 0; i--) {
|
|
if (entryStart(i, int32) < rangeStart) return null;
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) return token as Result;
|
|
}
|
|
} else {
|
|
for (let i = lastTokenIndex; i >= 0; i--) {
|
|
if (entryStart(i, int32) < rangeStart) return null;
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) {
|
|
if (skip <= 0) return token as Result;
|
|
skip--;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the last tokens of the given node.
|
|
* @param node - The AST node.
|
|
* @param countOptions? - Options object.
|
|
* If is a number, equivalent to `{ count: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns Array of `Token`s, or array of `Token | Comment`s if `includeComments` is `true`.
|
|
*/
|
|
export function getLastTokens<Options extends CountOptions | number | FilterFn | null | undefined>(
|
|
node: Node,
|
|
countOptions?: Options,
|
|
): TokenResult<Options>[] {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options>[];
|
|
|
|
// Maximum number of tokens to return
|
|
const count =
|
|
typeof countOptions === "number"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.count
|
|
: null;
|
|
|
|
// Function to filter tokens
|
|
const filter: FilterFn | null | undefined =
|
|
typeof countOptions === "function"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(countOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
const { range } = node,
|
|
rangeStart = range[0],
|
|
rangeEnd = range[1];
|
|
|
|
// Binary search for first token past `node`'s start
|
|
const sliceStart = firstTokenAtOrAfter(int32, rangeStart, 0, len);
|
|
// Binary search for first token past `node`'s end
|
|
const sliceEnd = firstTokenAtOrAfter(int32, rangeEnd, sliceStart, len);
|
|
|
|
if (typeof filter !== "function") {
|
|
if (typeof count !== "number") {
|
|
return collectEntries(sliceStart, sliceEnd, includeComments) as Result;
|
|
}
|
|
return collectEntries(
|
|
Math.max(sliceStart, sliceEnd - count),
|
|
sliceEnd,
|
|
includeComments,
|
|
) as Result;
|
|
}
|
|
|
|
const lastTokens: TokenOrComment[] = [];
|
|
if (typeof count !== "number") {
|
|
for (let i = sliceStart; i < sliceEnd; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) lastTokens.push(token);
|
|
}
|
|
} else {
|
|
// `count` is the number of tokens within range from the end, so we iterate in reverse
|
|
for (let i = sliceEnd - 1; i >= sliceStart && lastTokens.length < count; i--) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) lastTokens.unshift(token);
|
|
}
|
|
}
|
|
return lastTokens as Result;
|
|
}
|
|
|
|
/**
|
|
* Get the token that precedes a given node or token.
|
|
* @param nodeOrToken - The AST node or token.
|
|
* @param skipOptions? - Options object.
|
|
* If is a number, equivalent to `{ skip: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns `Token` (or `Token | Comment` if `includeComments` is `true`), or `null` if none found.
|
|
*/
|
|
export function getTokenBefore<Options extends SkipOptions | number | FilterFn | null | undefined>(
|
|
nodeOrToken: NodeOrToken,
|
|
skipOptions?: Options,
|
|
): TokenResult<Options> | null {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options> | null;
|
|
|
|
// Number of tokens preceding the given node to skip
|
|
let skip =
|
|
typeof skipOptions === "number"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.skip
|
|
: null;
|
|
|
|
const filter: FilterFn | null | undefined =
|
|
typeof skipOptions === "function"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(skipOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
const nodeStart = nodeOrToken.range[0];
|
|
|
|
// Binary search for token immediately before the given node, token, or comment
|
|
let beforeIndex = firstTokenAtOrAfter(int32, nodeStart, 0, len) - 1;
|
|
|
|
if (typeof filter !== "function") {
|
|
const skipTo = beforeIndex - (skip ?? 0);
|
|
if (skipTo < 0) return null;
|
|
return getEntry(skipTo, includeComments) as Result;
|
|
}
|
|
|
|
if (typeof skip !== "number") {
|
|
while (beforeIndex >= 0) {
|
|
const token = getEntry(beforeIndex, includeComments);
|
|
if (filter(token)) return token as Result;
|
|
beforeIndex--;
|
|
}
|
|
} else {
|
|
while (beforeIndex >= 0) {
|
|
const token = getEntry(beforeIndex, includeComments);
|
|
if (filter(token)) {
|
|
if (skip <= 0) return token as Result;
|
|
skip--;
|
|
}
|
|
beforeIndex--;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the token that precedes a given node or token.
|
|
*
|
|
* @deprecated Use `sourceCode.getTokenBefore` with `includeComments: true` instead.
|
|
*
|
|
* @param nodeOrToken The AST node or token.
|
|
* @param skip - Number of tokens to skip.
|
|
* @returns `TokenOrComment | null`.
|
|
*/
|
|
export function getTokenOrCommentBefore(
|
|
nodeOrToken: NodeOrToken,
|
|
skip?: number,
|
|
): TokenOrComment | null {
|
|
// Equivalent to `return getTokenBefore(nodeOrToken, { includeComments: true, skip });`,
|
|
// but reuse a global object to avoid creating a new object on each call
|
|
INCLUDE_COMMENTS_SKIP_OPTIONS.skip = skip;
|
|
return getTokenBefore(nodeOrToken, INCLUDE_COMMENTS_SKIP_OPTIONS);
|
|
}
|
|
|
|
/**
|
|
* Get the tokens that precede a given node or token.
|
|
* @param nodeOrToken - The AST node or token.
|
|
* @param countOptions? - Options object.
|
|
* If is a number, equivalent to `{ count: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns Array of `Token`s, or array of `Token | Comment`s if `includeComments` is `true`.
|
|
*/
|
|
export function getTokensBefore<
|
|
Options extends CountOptions | number | FilterFn | null | undefined,
|
|
>(nodeOrToken: NodeOrToken, countOptions?: Options): TokenResult<Options>[] {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options>[];
|
|
|
|
// Maximum number of tokens to return
|
|
const count =
|
|
typeof countOptions === "number"
|
|
? Math.max(0, countOptions)
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.count
|
|
: null;
|
|
|
|
// Function to filter tokens
|
|
const filter: FilterFn | null | undefined =
|
|
typeof countOptions === "function"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(countOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
const targetStart = nodeOrToken.range[0];
|
|
|
|
// Binary search for first token past `nodeOrToken`'s start
|
|
const sliceEnd = firstTokenAtOrAfter(int32, targetStart, 0, len);
|
|
|
|
// Fast path for the common case
|
|
if (typeof filter !== "function") {
|
|
if (typeof count !== "number") return collectEntries(0, sliceEnd, includeComments) as Result;
|
|
return collectEntries(Math.max(0, sliceEnd - count), sliceEnd, includeComments) as Result;
|
|
}
|
|
|
|
const tokensBefore: TokenOrComment[] = [];
|
|
if (typeof count !== "number") {
|
|
for (let i = 0; i < sliceEnd; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) tokensBefore.push(token);
|
|
}
|
|
} else {
|
|
// Count is the number of preceding tokens, so we iterate in reverse
|
|
for (let i = sliceEnd - 1; i >= 0 && tokensBefore.length < count; i--) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) tokensBefore.unshift(token);
|
|
}
|
|
}
|
|
return tokensBefore as Result;
|
|
}
|
|
|
|
/**
|
|
* Get the token that follows a given node or token.
|
|
* @param nodeOrToken - The AST node or token.
|
|
* @param skipOptions? - Options object.
|
|
* If is a number, equivalent to `{ skip: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns `Token` (or `Token | Comment` if `includeComments` is `true`), or `null` if none found.
|
|
*/
|
|
export function getTokenAfter<Options extends SkipOptions | number | FilterFn | null | undefined>(
|
|
nodeOrToken: NodeOrToken,
|
|
skipOptions?: Options,
|
|
): TokenResult<Options> | null {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options> | null;
|
|
|
|
// Number of tokens following the given node to skip
|
|
let skip =
|
|
typeof skipOptions === "number"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.skip
|
|
: null;
|
|
|
|
const filter: FilterFn | null | undefined =
|
|
typeof skipOptions === "function"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(skipOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
const rangeEnd = nodeOrToken.range[1];
|
|
|
|
// Binary search for first token past `nodeOrToken`'s end
|
|
const startIndex = firstTokenAtOrAfter(int32, rangeEnd, 0, len);
|
|
|
|
// Fast path for the common case
|
|
if (typeof filter !== "function") {
|
|
const skipTo = startIndex + (skip ?? 0);
|
|
if (skipTo >= len) return null;
|
|
return getEntry(skipTo, includeComments) as Result;
|
|
}
|
|
|
|
if (typeof skip !== "number") {
|
|
for (let i = startIndex; i < len; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) return token as Result;
|
|
}
|
|
} else {
|
|
for (let i = startIndex; i < len; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) {
|
|
if (skip <= 0) return token as Result;
|
|
skip--;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the token that follows a given node or token.
|
|
*
|
|
* @deprecated Use `sourceCode.getTokenAfter` with `includeComments: true` instead.
|
|
*
|
|
* @param nodeOrToken The AST node or token.
|
|
* @param skip - Number of tokens to skip.
|
|
* @returns `TokenOrComment | null`.
|
|
*/
|
|
export function getTokenOrCommentAfter(
|
|
nodeOrToken: NodeOrToken,
|
|
skip?: number,
|
|
): TokenOrComment | null {
|
|
// Equivalent to `return getTokenAfter(nodeOrToken, { includeComments: true, skip });`,
|
|
// but reuse a global object to avoid creating a new object on each call
|
|
INCLUDE_COMMENTS_SKIP_OPTIONS.skip = skip;
|
|
return getTokenAfter(nodeOrToken, INCLUDE_COMMENTS_SKIP_OPTIONS);
|
|
}
|
|
|
|
/**
|
|
* Get the tokens that follow a given node or token.
|
|
* @param nodeOrToken - The AST node or token.
|
|
* @param countOptions? - Options object.
|
|
* If is a number, equivalent to `{ count: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns Array of `Token`s, or array of `Token | Comment`s if `includeComments` is `true`.
|
|
*/
|
|
export function getTokensAfter<Options extends CountOptions | number | FilterFn | null | undefined>(
|
|
nodeOrToken: NodeOrToken,
|
|
countOptions?: Options,
|
|
): TokenResult<Options>[] {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options>[];
|
|
|
|
const count =
|
|
typeof countOptions === "number"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.count
|
|
: null;
|
|
|
|
const filter: FilterFn | null | undefined =
|
|
typeof countOptions === "function"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(countOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
const rangeEnd = nodeOrToken.range[1];
|
|
|
|
// Binary search for first token past `nodeOrToken`'s end
|
|
const sliceStart = firstTokenAtOrAfter(int32, rangeEnd, 0, len);
|
|
|
|
// Fast path for the common case
|
|
if (typeof filter !== "function") {
|
|
if (typeof count !== "number") {
|
|
return collectEntries(sliceStart, len, includeComments) as Result;
|
|
}
|
|
return collectEntries(sliceStart, Math.min(sliceStart + count, len), includeComments) as Result;
|
|
}
|
|
|
|
const tokenListAfter: TokenOrComment[] = [];
|
|
if (typeof count !== "number") {
|
|
for (let i = sliceStart; i < len; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) tokenListAfter.push(token);
|
|
}
|
|
} else {
|
|
for (let i = sliceStart; i < len && tokenListAfter.length < count; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) tokenListAfter.push(token);
|
|
}
|
|
}
|
|
return tokenListAfter as Result;
|
|
}
|
|
|
|
/**
|
|
* Get all of the tokens between two non-overlapping nodes.
|
|
* @param left - Node or token before the desired token range.
|
|
* @param right - Node or token after the desired token range.
|
|
* @param countOptions? - Options object. If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns Array of `Token`s, or array of `Token | Comment`s if `includeComments` is `true`.
|
|
*/
|
|
/**
|
|
* Get all of the tokens between two non-overlapping nodes.
|
|
* @param left - Node or token before the desired token range.
|
|
* @param right - Node or token after the desired token range.
|
|
* @param padding - Number of extra tokens on either side of center.
|
|
* @returns Array of `Token`s, or array of `Token | Comment`s if `includeComments` is `true`.
|
|
*/
|
|
export function getTokensBetween<
|
|
Options extends CountOptions | number | FilterFn | null | undefined,
|
|
>(left: NodeOrToken, right: NodeOrToken, countOptions?: Options): TokenResult<Options>[] {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options>[];
|
|
|
|
const count =
|
|
typeof countOptions === "object" && countOptions !== null ? countOptions.count : null;
|
|
|
|
const padding = typeof countOptions === "number" ? countOptions : 0;
|
|
|
|
const filter: FilterFn | null | undefined =
|
|
typeof countOptions === "function"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(countOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
// This range is not invariant over node order.
|
|
// The first argument must be the left node.
|
|
// Same as ESLint's implementation.
|
|
const rangeStart = left.range[1],
|
|
rangeEnd = right.range[0];
|
|
|
|
// Binary search for first token past "between" range start
|
|
let sliceStart = firstTokenAtOrAfter(int32, rangeStart, 0, len);
|
|
// Binary search for first token past "between" range end
|
|
let sliceEnd = firstTokenAtOrAfter(int32, rangeEnd, sliceStart, len);
|
|
|
|
// Apply padding
|
|
sliceStart = Math.max(0, sliceStart - padding);
|
|
sliceEnd = Math.min(sliceEnd + padding, len);
|
|
|
|
if (typeof filter !== "function") {
|
|
if (typeof count !== "number") {
|
|
return collectEntries(sliceStart, sliceEnd, includeComments) as Result;
|
|
}
|
|
return collectEntries(
|
|
sliceStart,
|
|
Math.min(sliceStart + count, sliceEnd),
|
|
includeComments,
|
|
) as Result;
|
|
}
|
|
|
|
const tokensBetween: TokenOrComment[] = [];
|
|
if (typeof count !== "number") {
|
|
for (let i = sliceStart; i < sliceEnd; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) tokensBetween.push(token);
|
|
}
|
|
} else {
|
|
for (let i = sliceStart; i < sliceEnd && tokensBetween.length < count; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) tokensBetween.push(token);
|
|
}
|
|
}
|
|
return tokensBetween as Result;
|
|
}
|
|
|
|
/**
|
|
* Get the first token between two non-overlapping nodes.
|
|
* @param left - Node or token before the desired token range.
|
|
* @param right - Node or token after the desired token range.
|
|
* @param skipOptions? - Options object.
|
|
* If is a number, equivalent to `{ skip: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns `Token` (or `Token | Comment` if `includeComments` is `true`), or `null` if none found.
|
|
*/
|
|
export function getFirstTokenBetween<
|
|
Options extends SkipOptions | number | FilterFn | null | undefined,
|
|
>(left: NodeOrToken, right: NodeOrToken, skipOptions?: Options): TokenResult<Options> | null {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options> | null;
|
|
|
|
// Number of tokens at the beginning of the "between" range to skip
|
|
let skip =
|
|
typeof skipOptions === "number"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.skip
|
|
: null;
|
|
|
|
const filter: FilterFn | null | undefined =
|
|
typeof skipOptions === "function"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(skipOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
// This range is not invariant over node order.
|
|
// The first argument must be the left node.
|
|
// Same as ESLint's implementation.
|
|
const rangeStart = left.range[1],
|
|
rangeEnd = right.range[0];
|
|
|
|
// Binary search for token immediately following `left`
|
|
const firstTokenIndex = firstTokenAtOrAfter(int32, rangeStart, 0, len);
|
|
|
|
if (typeof filter !== "function") {
|
|
const skipTo = firstTokenIndex + (skip ?? 0);
|
|
if (skipTo >= len) return null;
|
|
if (entryStart(skipTo, int32) >= rangeEnd) return null;
|
|
return getEntry(skipTo, includeComments) as Result;
|
|
}
|
|
|
|
if (typeof skip !== "number") {
|
|
for (let i = firstTokenIndex; i < len; i++) {
|
|
if (entryStart(i, int32) >= rangeEnd) return null;
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) return token as Result;
|
|
}
|
|
} else {
|
|
for (let i = firstTokenIndex; i < len; i++) {
|
|
if (entryStart(i, int32) >= rangeEnd) return null;
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) {
|
|
if (skip <= 0) return token as Result;
|
|
skip--;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the first tokens between two non-overlapping nodes.
|
|
* @param left - Node or token before the desired token range.
|
|
* @param right - Node or token after the desired token range.
|
|
* @param countOptions? - Options object.
|
|
* If is a number, equivalent to `{ count: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns Array of `Token`s, or array of `Token | Comment`s if `includeComments` is `true`.
|
|
*/
|
|
export function getFirstTokensBetween<
|
|
Options extends CountOptions | number | FilterFn | null | undefined,
|
|
>(left: NodeOrToken, right: NodeOrToken, countOptions?: Options): TokenResult<Options>[] {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options>[];
|
|
|
|
const count =
|
|
typeof countOptions === "number"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.count
|
|
: null;
|
|
|
|
const filter: FilterFn | null | undefined =
|
|
typeof countOptions === "function"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(countOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
// This range is not invariant over node order.
|
|
// The first argument must be the left node.
|
|
// Same as ESLint's implementation.
|
|
const rangeStart = left.range[1],
|
|
rangeEnd = right.range[0];
|
|
|
|
// Find the first token after `left`
|
|
const sliceStart = firstTokenAtOrAfter(int32, rangeStart, 0, len);
|
|
// Find the first token at or after `right`
|
|
const sliceEnd = firstTokenAtOrAfter(int32, rangeEnd, sliceStart, len);
|
|
|
|
if (typeof filter !== "function") {
|
|
if (typeof count !== "number") {
|
|
return collectEntries(sliceStart, sliceEnd, includeComments) as Result;
|
|
}
|
|
return collectEntries(
|
|
sliceStart,
|
|
Math.min(sliceStart + count, sliceEnd),
|
|
includeComments,
|
|
) as Result;
|
|
}
|
|
|
|
const firstTokens: TokenOrComment[] = [];
|
|
if (typeof count !== "number") {
|
|
for (let i = sliceStart; i < sliceEnd; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) firstTokens.push(token);
|
|
}
|
|
} else {
|
|
for (let i = sliceStart; i < sliceEnd && firstTokens.length < count; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) firstTokens.push(token);
|
|
}
|
|
}
|
|
return firstTokens as Result;
|
|
}
|
|
|
|
/**
|
|
* Get the last token between two non-overlapping nodes.
|
|
* @param left - Node or token before the desired token range.
|
|
* @param right - Node or token after the desired token range.
|
|
* @param skipOptions? - Options object.
|
|
* If is a number, equivalent to `{ skip: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns `Token` (or `Token | Comment` if `includeComments` is `true`), or `null` if none found.
|
|
*/
|
|
export function getLastTokenBetween<
|
|
Options extends SkipOptions | number | FilterFn | null | undefined,
|
|
>(left: NodeOrToken, right: NodeOrToken, skipOptions?: Options): TokenResult<Options> | null {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options> | null;
|
|
|
|
// Number of tokens at the end of the "between" range to skip
|
|
let skip =
|
|
typeof skipOptions === "number"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.skip
|
|
: null;
|
|
|
|
const filter: FilterFn | null | undefined =
|
|
typeof skipOptions === "function"
|
|
? skipOptions
|
|
: typeof skipOptions === "object" && skipOptions !== null
|
|
? skipOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(skipOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
// This range is not invariant over node order.
|
|
// The first argument must be the left node.
|
|
// Same as ESLint's implementation.
|
|
const rangeStart = left.range[1],
|
|
rangeEnd = right.range[0];
|
|
|
|
// Binary search for token immediately preceding `right`.
|
|
// The found token may be within the left node if there are no entries between the nodes.
|
|
const lastTokenIndex = firstTokenAtOrAfter(int32, rangeEnd, 0, len) - 1;
|
|
|
|
// Fast path for the common case
|
|
if (typeof filter !== "function") {
|
|
const skipTo = lastTokenIndex - (skip ?? 0);
|
|
if (skipTo < 0) return null;
|
|
if (entryStart(skipTo, int32) < rangeStart) return null;
|
|
return getEntry(skipTo, includeComments) as Result;
|
|
}
|
|
|
|
if (typeof skip !== "number") {
|
|
for (let i = lastTokenIndex; i >= 0; i--) {
|
|
if (entryStart(i, int32) < rangeStart) return null;
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) return token as Result;
|
|
}
|
|
} else {
|
|
for (let i = lastTokenIndex; i >= 0; i--) {
|
|
if (entryStart(i, int32) < rangeStart) return null;
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) {
|
|
if (skip <= 0) return token as Result;
|
|
skip--;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the last tokens between two non-overlapping nodes.
|
|
* @param left - Node or token before the desired token range.
|
|
* @param right - Node or token after the desired token range.
|
|
* @param countOptions? - Options object.
|
|
* If is a number, equivalent to `{ count: n }`.
|
|
* If is a function, equivalent to `{ filter: fn }`.
|
|
* @returns Array of `Token`s, or array of `Token | Comment`s if `includeComments` is `true`.
|
|
*/
|
|
export function getLastTokensBetween<
|
|
Options extends CountOptions | number | FilterFn | null | undefined,
|
|
>(left: NodeOrToken, right: NodeOrToken, countOptions?: Options): TokenResult<Options>[] {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options>[];
|
|
|
|
const count =
|
|
typeof countOptions === "number"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.count
|
|
: null;
|
|
|
|
const filter: FilterFn | null | undefined =
|
|
typeof countOptions === "function"
|
|
? countOptions
|
|
: typeof countOptions === "object" && countOptions !== null
|
|
? countOptions.filter
|
|
: null;
|
|
|
|
const includeComments = getIncludeComments(countOptions);
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
// This range is not invariant over node order.
|
|
// The first argument must be the left node.
|
|
// Same as ESLint's implementation.
|
|
const rangeStart = left.range[1],
|
|
rangeEnd = right.range[0];
|
|
|
|
// Binary search for first token past "between" range start
|
|
const sliceStart = firstTokenAtOrAfter(int32, rangeStart, 0, len);
|
|
// Binary search for first token past "between" range end
|
|
const sliceEnd = firstTokenAtOrAfter(int32, rangeEnd, sliceStart, len);
|
|
|
|
// Fast path for the common case
|
|
if (typeof filter !== "function") {
|
|
if (typeof count !== "number") {
|
|
return collectEntries(sliceStart, sliceEnd, includeComments) as Result;
|
|
}
|
|
return collectEntries(
|
|
Math.max(sliceStart, sliceEnd - count),
|
|
sliceEnd,
|
|
includeComments,
|
|
) as Result;
|
|
}
|
|
|
|
const tokensBetween: TokenOrComment[] = [];
|
|
if (typeof count !== "number") {
|
|
for (let i = sliceStart; i < sliceEnd; i++) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) tokensBetween.push(token);
|
|
}
|
|
} else {
|
|
// Count is the number of preceding tokens, so we iterate in reverse
|
|
for (let i = sliceEnd - 1; i >= sliceStart && tokensBetween.length < count; i--) {
|
|
const token = getEntry(i, includeComments);
|
|
if (filter(token)) tokensBetween.unshift(token);
|
|
}
|
|
}
|
|
return tokensBetween as Result;
|
|
}
|
|
|
|
/**
|
|
* Get the token starting at the specified index.
|
|
* @param offset - Start offset of the token.
|
|
* @param rangeOptions - Options object.
|
|
* @returns `Token` (or `Token | Comment` if `includeComments` is `true`), or `null` if none found.
|
|
*/
|
|
export function getTokenByRangeStart<Options extends RangeOptions | null | undefined>(
|
|
offset: number,
|
|
rangeOptions?: Options,
|
|
): TokenResult<Options> | null {
|
|
// TypeScript cannot verify conditional return types within the function body,
|
|
// so we use `Result` alias + casts on return statements
|
|
type Result = TokenResult<Options> | null;
|
|
|
|
const includeComments =
|
|
typeof rangeOptions === "object"
|
|
&& rangeOptions !== null
|
|
&& "includeComments" in rangeOptions
|
|
&& !!rangeOptions.includeComments;
|
|
|
|
let int32: Int32Array, len: number;
|
|
if (includeComments === false) {
|
|
if (tokensInt32 === null) initTokensBuffer();
|
|
debugAssertIsNonNull(tokensInt32);
|
|
int32 = tokensInt32;
|
|
len = tokensLen;
|
|
} else {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
int32 = tokensAndCommentsInt32;
|
|
len = tokensAndCommentsLen;
|
|
}
|
|
|
|
// Binary search for token starting at the given index.
|
|
//
|
|
// Note: Source text is limited to 1 GiB max, so offsets cannot exceed 2^30.
|
|
// This makes it safe to use `>> 1` for division by 2 (which is faster than `>>> 1`).
|
|
for (let lo = 0, hi = len; lo < hi;) {
|
|
const mid = (lo + hi) >> 1;
|
|
const tokenStart = int32[mid << 2];
|
|
if (tokenStart < offset) {
|
|
lo = mid + 1;
|
|
} else if (tokenStart > offset) {
|
|
hi = mid;
|
|
} else {
|
|
return getEntry(mid, includeComments) as Result;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const JSX_WHITESPACE_REGEXP = /\s/u;
|
|
|
|
/**
|
|
* Determine if two nodes or tokens have at least one whitespace character between them.
|
|
* Order does not matter.
|
|
*
|
|
* Returns `false` if the given nodes or tokens overlap.
|
|
*
|
|
* Checks for whitespace *between tokens*, not including whitespace *inside tokens*.
|
|
* e.g. Returns `false` for `isSpaceBetween(x, y)` in `x+" "+y`.
|
|
*
|
|
* @param first - The first node or token to check between.
|
|
* @param second - The second node or token to check between.
|
|
* @returns `true` if there is a whitespace character between
|
|
* any of the tokens found between the two given nodes or tokens.
|
|
*/
|
|
export function isSpaceBetween(first: NodeOrToken, second: NodeOrToken): boolean {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
|
|
const range1 = first.range,
|
|
range2 = second.range;
|
|
|
|
// Find the range between the two nodes/tokens.
|
|
//
|
|
// Unlike other methods which require the user to pass the nodes in order of appearance,
|
|
// `isSpaceBetween()` is invariant over the sequence of the two nodes.
|
|
//
|
|
// 1 node/token can completely enclose another, but they can't *partially* overlap.
|
|
// ```
|
|
// Possible:
|
|
// |------------|
|
|
// |------|
|
|
//
|
|
// Impossible:
|
|
// |------------|
|
|
// |------------|
|
|
// ```
|
|
// We use that invariant to reduce this to a single branch.
|
|
let rangeStart: number = range1[0],
|
|
rangeEnd: number = range2[0];
|
|
if (rangeStart < rangeEnd) {
|
|
rangeStart = range1[1];
|
|
} else {
|
|
rangeEnd = rangeStart;
|
|
rangeStart = range2[1];
|
|
}
|
|
|
|
// Binary search for the first token past `rangeStart`.
|
|
// Unless `first` and `second` are adjacent or overlapping,
|
|
// the token will be the first token/comment between the two nodes.
|
|
let index = firstTokenAtOrAfter(tokensAndCommentsInt32, rangeStart, 0, tokensAndCommentsLen);
|
|
|
|
for (let lastTokenEnd = rangeStart; index < tokensAndCommentsLen; index++) {
|
|
const tokenStart = tokensAndCommentsInt32[index << 2];
|
|
// The first token of the later node should undergo the check in the second branch
|
|
if (tokenStart > rangeEnd) break;
|
|
if (tokenStart !== lastTokenEnd) return true;
|
|
lastTokenEnd = entryEnd(index, true);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determine if two nodes or tokens have at least one whitespace character between them.
|
|
* Order does not matter.
|
|
*
|
|
* Returns `false` if the given nodes or tokens overlap.
|
|
*
|
|
* Checks for whitespace *between tokens*, not including whitespace *inside tokens*.
|
|
* e.g. Returns `false` for `isSpaceBetween(x, y)` in `x+" "+y`.
|
|
*
|
|
* Unlike `SourceCode#isSpaceBetween`, this function does return `true` if there is a `JSText` token between the two
|
|
* input tokens, and it contains whitespace.
|
|
* e.g. Returns `true` for `isSpaceBetweenTokens(x, slash)` in `<X>a b</X>`.
|
|
*
|
|
* @deprecated Use `sourceCode.isSpaceBetween` instead.
|
|
*
|
|
* @param first - The first node or token to check between.
|
|
* @param second - The second node or token to check between.
|
|
* @returns `true` if there is a whitespace character between
|
|
* any of the tokens found between the two given nodes or tokens.
|
|
*/
|
|
export function isSpaceBetweenTokens(first: NodeOrToken, second: NodeOrToken): boolean {
|
|
if (tokensAndCommentsInt32 === null) initTokensAndCommentsBuffer();
|
|
debugAssertIsNonNull(tokensAndCommentsInt32);
|
|
|
|
const range1 = first.range,
|
|
range2 = second.range;
|
|
|
|
// Find the range between the two nodes/tokens.
|
|
// Unlike other methods which require the user to pass the nodes in order of appearance,
|
|
// `isSpaceBetweenTokens()` is invariant over the sequence of the two nodes.
|
|
// See comment in `isSpaceBetween` about why this is a single branch.
|
|
let rangeStart: number = range1[0],
|
|
rangeEnd: number = range2[0];
|
|
if (rangeStart < rangeEnd) {
|
|
rangeStart = range1[1];
|
|
} else {
|
|
rangeEnd = rangeStart;
|
|
rangeStart = range2[1];
|
|
}
|
|
|
|
// Binary search for the first token past `rangeStart`.
|
|
// Unless `first` and `second` are adjacent or overlapping,
|
|
// the token will be the first token/comment between the two nodes.
|
|
let index = firstTokenAtOrAfter(tokensAndCommentsInt32, rangeStart, 0, tokensAndCommentsLen);
|
|
|
|
for (let lastTokenEnd = rangeStart; index < tokensAndCommentsLen; index++) {
|
|
const tokenStart = tokensAndCommentsInt32[index << 2];
|
|
|
|
// The first token of the later node should undergo the check in the second branch
|
|
if (tokenStart > rangeEnd) break;
|
|
|
|
// Deserialize to check type/value for JSXText whitespace detection
|
|
const token = getTokenOrComment(index);
|
|
if (
|
|
tokenStart !== lastTokenEnd
|
|
|| (tokenStart < rangeEnd
|
|
&& token.type === "JSXText"
|
|
&& JSX_WHITESPACE_REGEXP.test(token.value))
|
|
) {
|
|
return true;
|
|
}
|
|
lastTokenEnd = token.end;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Extract `includeComments` boolean from options.
|
|
*
|
|
* @param options - Options object, number, function, or nullish
|
|
* @returns `true` if `options` has `includeComments: true`
|
|
*/
|
|
function getIncludeComments(
|
|
options: SkipOptions | CountOptions | number | FilterFn | null | undefined,
|
|
): boolean {
|
|
return (
|
|
typeof options === "object"
|
|
&& options !== null
|
|
&& "includeComments" in options
|
|
&& !!options.includeComments
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get a token at `index`, deserializing if needed.
|
|
* For `includeComments` mode, gets from the merged buffer instead.
|
|
*
|
|
* @param index - Entry index in the tokens or merged buffer
|
|
* @param includeComments - Whether to use the merged tokens-and-comments buffer
|
|
* @returns Deserialized token or comment
|
|
*/
|
|
function getEntry(index: number, includeComments: boolean): TokenOrComment {
|
|
return includeComments === true ? getTokenOrComment(index) : getToken(index);
|
|
}
|
|
|
|
/**
|
|
* Get `start` offset of token/comment at `index` from the given buffer.
|
|
*
|
|
* @param index - Entry index
|
|
* @param int32 - The `Int32Array` buffer (tokens or merged)
|
|
* @returns Start offset in source text
|
|
*/
|
|
function entryStart(index: number, int32: Int32Array): number {
|
|
return int32[index << 2];
|
|
}
|
|
|
|
/**
|
|
* Get `end` offset of token/comment at `index`.
|
|
* For tokens-only, reads from `tokensInt32`. For `includeComments`, looks up from the original buffer.
|
|
*
|
|
* @param index - Entry index in the tokens or merged buffer
|
|
* @param includeComments - Whether to use the merged tokens-and-comments buffer
|
|
* @returns End offset in source text
|
|
*/
|
|
function entryEnd(index: number, includeComments: boolean): number {
|
|
return includeComments === true ? getTokenOrCommentEnd(index) : tokensInt32![(index << 2) + 1];
|
|
}
|
|
|
|
/**
|
|
* Collect tokens/comments from `startIndex` (inclusive) to `endIndex` (exclusive) into an array.
|
|
* Deserializes each token/comment on demand.
|
|
*
|
|
* For tokens-only mode, batch-deserializes then slices `cachedTokens`.
|
|
* For `includeComments` mode, builds the array entry by entry from the merged buffer.
|
|
*
|
|
* @param startIndex - First entry index (inclusive)
|
|
* @param endIndex - Last entry index (exclusive)
|
|
* @param includeComments - Whether to use the merged tokens-and-comments buffer
|
|
* @returns Array of tokens (and optionally comments)
|
|
*/
|
|
function collectEntries(
|
|
startIndex: number,
|
|
endIndex: number,
|
|
includeComments: boolean,
|
|
): TokenOrComment[] {
|
|
if (includeComments === false) {
|
|
// Batch-deserialize tokens in range, then slice from `cachedTokens` array
|
|
for (let i = startIndex; i < endIndex; i++) {
|
|
getToken(i);
|
|
}
|
|
return cachedTokens!.slice(startIndex, endIndex) as TokenOrComment[];
|
|
}
|
|
|
|
const len = endIndex - startIndex;
|
|
if (len === 0) return [];
|
|
|
|
// Pre-allocate with correct size. Write `null` into first entry to transition to PACKED_ELEMENTS before the loop.
|
|
// oxlint-disable-next-line unicorn/no-new-array
|
|
const tokensAndCommentsSubset: TokenOrComment[] = new Array(len).fill(0);
|
|
tokensAndCommentsSubset[0] = null!;
|
|
let i = 0;
|
|
do {
|
|
tokensAndCommentsSubset[i] = getTokenOrComment(startIndex + i);
|
|
} while (++i < len);
|
|
return tokensAndCommentsSubset;
|
|
}
|
|
|
|
/**
|
|
* Find the index of the first entry in a `Int32Array` buffer whose `start` is >= `offset`, via binary search.
|
|
*
|
|
* Each entry occupies 4 x u32s (16 bytes), with `start` as the first u32.
|
|
* Searched range starts at `startIndex` and ends at `length`.
|
|
*
|
|
* Returns `length` if all entries have `start` < `offset`.
|
|
*
|
|
* Note: Source text is limited to 1 GiB max, so number of tokens cannot exceed 2^30.
|
|
* This makes it safe to use `>> 1` for division by 2 below (which is faster than `>>> 1`).
|
|
*
|
|
* @param int32 - `Int32Array` buffer (tokens, comments, or tokensAndComments)
|
|
* @param offset - Source offset to search for
|
|
* @param startIndex - Starting entry index for the search
|
|
* @param length - Total number of entries in the buffer
|
|
* @returns Index of first entry with `start >= offset`
|
|
*/
|
|
export function firstTokenAtOrAfter(
|
|
int32: Int32Array,
|
|
offset: number,
|
|
startIndex: number,
|
|
length: number,
|
|
): number {
|
|
for (let endIndex = length; startIndex < endIndex;) {
|
|
const mid = (startIndex + endIndex) >> 1;
|
|
if (int32[mid << 2] < offset) {
|
|
startIndex = mid + 1;
|
|
} else {
|
|
endIndex = mid;
|
|
}
|
|
}
|
|
return startIndex;
|
|
}
|