Files
oxc-project__oxc/apps/oxlint/src-js/plugins/tokens_methods.ts
2026-08-20 14:36:47 +00:00

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