diff --git a/apps/oxlint/src-js/plugins/tokens_methods.ts b/apps/oxlint/src-js/plugins/tokens_methods.ts index fdad8754c5b71..3e3be42dfed23 100644 --- a/apps/oxlint/src-js/plugins/tokens_methods.ts +++ b/apps/oxlint/src-js/plugins/tokens_methods.ts @@ -6,7 +6,7 @@ import { tokens, tokensAndComments, initTokens, initTokensAndComments } from "./ import { debugAssertIsNonNull } from "../utils/asserts.ts"; import type { Node, NodeOrToken } from "./types.ts"; -import type { TokenOrComment } from "./tokens.ts"; +import type { Token, TokenOrComment } from "./tokens.ts"; /** * Options for various `SourceCode` methods e.g. `getFirstToken`. @@ -45,6 +45,22 @@ export interface RangeOptions { */ 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 extends { includeComments: false } + ? false + : "includeComments" extends keyof Options + ? true + : false; + +/** + * Resolves to `TokenOrComment` if `Options` may include comments, `Token` otherwise. + */ +type TokenResult = MayIncludeComments 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 }; @@ -62,11 +78,15 @@ const INCLUDE_COMMENTS_SKIP_OPTIONS: SkipOptions = { includeComments: true, skip * @param afterCount? - The number of tokens after the node to retrieve. * @returns Array of `Token`s. */ -export function getTokens( +export function getTokens( node: Node, - countOptions?: CountOptions | number | FilterFn | null, + countOptions?: Options, afterCount?: number | null, -): TokenOrComment[] { +): TokenResult[] { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult[]; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -84,7 +104,7 @@ export function getTokens( : 0; // Function to filter tokens - const filter = + const filter: FilterFn | null | undefined = typeof countOptions === "function" ? countOptions : typeof countOptions === "object" && countOptions !== null @@ -139,7 +159,10 @@ export function getTokens( sliceEnd = Math.min(sliceEnd + afterCount, tokensLength); if (typeof filter !== "function") { - return tokenList.slice(sliceStart, Math.min(sliceStart + (count ?? sliceEnd), sliceEnd)); + return tokenList.slice( + sliceStart, + Math.min(sliceStart + (count ?? sliceEnd), sliceEnd), + ) as Result; } const allTokens: TokenOrComment[] = []; @@ -149,7 +172,7 @@ export function getTokens( const token = tokenList[i]; if (filter(token)) allTokens.push(token); } - return allTokens; + return allTokens as Result; } for (let i = sliceStart; i < sliceEnd && count > 0; i++) { @@ -160,7 +183,7 @@ export function getTokens( } } - return allTokens; + return allTokens as Result; } /** @@ -171,10 +194,14 @@ export function getTokens( * If is a function, equivalent to `{ filter: fn }`. * @returns `Token`, or `null` if all were skipped. */ -export function getFirstToken( +export function getFirstToken( node: Node, - skipOptions?: SkipOptions | number | FilterFn | null, -): TokenOrComment | null { + skipOptions?: Options, +): TokenResult | null { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult | null; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -187,7 +214,7 @@ export function getFirstToken( : null; // Filter function - const filter = + const filter: FilterFn | null | undefined = typeof skipOptions === "function" ? skipOptions : typeof skipOptions === "object" && skipOptions !== null @@ -234,21 +261,21 @@ export function getFirstToken( const token = tokenList[skipTo]; if (token.start >= rangeEnd) return null; - return token; + return token as Result; } if (typeof skip !== "number") { for (let i = startIndex; i < tokensLength; i++) { const token = tokenList[i]; if (token.start >= rangeEnd) return null; // Token is outside the node - if (filter(token)) return token; + if (filter(token)) return token as Result; } } else { for (let i = startIndex; i < tokensLength; i++) { const token = tokenList[i]; if (token.start >= rangeEnd) return null; // Token is outside the node if (filter(token)) { - if (skip <= 0) return token; + if (skip <= 0) return token as Result; skip--; } } @@ -265,10 +292,14 @@ export function getFirstToken( * If is a function, equivalent to `{ filter: fn }`. * @returns Array of `Token`s. */ -export function getFirstTokens( +export function getFirstTokens( node: Node, - countOptions?: CountOptions | number | FilterFn | null, -): TokenOrComment[] { + countOptions?: Options, +): TokenResult[] { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult[]; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -279,7 +310,7 @@ export function getFirstTokens( ? countOptions.count : null; - const filter = + const filter: FilterFn | null | undefined = typeof countOptions === "function" ? countOptions : typeof countOptions === "object" && countOptions !== null @@ -329,8 +360,8 @@ export function getFirstTokens( } if (typeof filter !== "function") { - if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd); - return tokenList.slice(sliceStart, Math.min(sliceStart + count, sliceEnd)); + if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd) as Result; + return tokenList.slice(sliceStart, Math.min(sliceStart + count, sliceEnd)) as Result; } const firstTokens: TokenOrComment[] = []; @@ -345,7 +376,7 @@ export function getFirstTokens( if (filter(token)) firstTokens.push(token); } } - return firstTokens; + return firstTokens as Result; } /** @@ -356,10 +387,14 @@ export function getFirstTokens( * If is a function, equivalent to `{ filter: fn }`. * @returns `Token`, or `null` if all were skipped. */ -export function getLastToken( +export function getLastToken( node: Node, - skipOptions?: SkipOptions | number | FilterFn | null, -): TokenOrComment | null { + skipOptions?: Options, +): TokenResult | null { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult | null; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -371,7 +406,7 @@ export function getLastToken( ? skipOptions.skip : null; - const filter = + const filter: FilterFn | null | undefined = typeof skipOptions === "function" ? skipOptions : typeof skipOptions === "object" && skipOptions !== null @@ -419,21 +454,21 @@ export function getLastToken( if (skipTo < 0) return null; const token = tokenList[skipTo]; if (token.start < rangeStart) return null; - return token; + return token as Result; } if (typeof skip !== "number") { for (let i = lastTokenIndex; i >= 0; i--) { const token = tokenList[i]; if (token.start < rangeStart) return null; - if (filter(token)) return token; + if (filter(token)) return token as Result; } } else { for (let i = lastTokenIndex; i >= 0; i--) { const token = tokenList[i]; if (token.start < rangeStart) return null; if (filter(token)) { - if (skip <= 0) return token; + if (skip <= 0) return token as Result; skip--; } } @@ -450,10 +485,14 @@ export function getLastToken( * If is a function, equivalent to `{ filter: fn }`. * @returns Array of `Token`s. */ -export function getLastTokens( +export function getLastTokens( node: Node, - countOptions?: CountOptions | number | FilterFn | null, -): TokenOrComment[] { + countOptions?: Options, +): TokenResult[] { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult[]; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -466,7 +505,7 @@ export function getLastTokens( : null; // Function to filter tokens - const filter = + const filter: FilterFn | null | undefined = typeof countOptions === "function" ? countOptions : typeof countOptions === "object" && countOptions !== null @@ -518,8 +557,8 @@ export function getLastTokens( } if (typeof filter !== "function") { - if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd); - return tokenList.slice(Math.max(sliceStart, sliceEnd - count), sliceEnd); + if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd) as Result; + return tokenList.slice(Math.max(sliceStart, sliceEnd - count), sliceEnd) as Result; } const lastTokens: TokenOrComment[] = []; @@ -535,7 +574,7 @@ export function getLastTokens( if (filter(token)) lastTokens.unshift(token); } } - return lastTokens; + return lastTokens as Result; } /** @@ -546,10 +585,14 @@ export function getLastTokens( * If is a function, equivalent to `{ filter: fn }`. * @returns `Token`, or `null` if all were skipped. */ -export function getTokenBefore( +export function getTokenBefore( nodeOrToken: NodeOrToken, - skipOptions?: SkipOptions | number | FilterFn | null, -): TokenOrComment | null { + skipOptions?: Options, +): TokenResult | null { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult | null; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -561,7 +604,7 @@ export function getTokenBefore( ? skipOptions.skip : null; - const filter = + const filter: FilterFn | null | undefined = typeof skipOptions === "function" ? skipOptions : typeof skipOptions === "object" && skipOptions !== null @@ -606,20 +649,20 @@ export function getTokenBefore( const skipTo = beforeIndex - (skip ?? 0); // Avoid indexing out of bounds if (skipTo < 0) return null; - return tokenList[skipTo]; + return tokenList[skipTo] as Result; } if (typeof skip !== "number") { while (beforeIndex >= 0) { const token = tokenList[beforeIndex]; - if (filter(token)) return token; + if (filter(token)) return token as Result; beforeIndex--; } } else { while (beforeIndex >= 0) { const token = tokenList[beforeIndex]; if (filter(token)) { - if (skip <= 0) return token; + if (skip <= 0) return token as Result; skip--; } beforeIndex--; @@ -656,10 +699,13 @@ export function getTokenOrCommentBefore( * If is a function, equivalent to `{ filter: fn }`. * @returns Array of `Token`s. */ -export function getTokensBefore( - nodeOrToken: NodeOrToken, - countOptions?: CountOptions | number | FilterFn | null, -): TokenOrComment[] { +export function getTokensBefore< + Options extends CountOptions | number | FilterFn | null | undefined, +>(nodeOrToken: NodeOrToken, countOptions?: Options): TokenResult[] { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult[]; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -672,7 +718,7 @@ export function getTokensBefore( : null; // Function to filter tokens - const filter = + const filter: FilterFn | null | undefined = typeof countOptions === "function" ? countOptions : typeof countOptions === "object" && countOptions !== null @@ -711,8 +757,8 @@ export function getTokensBefore( // Fast path for the common case if (typeof filter !== "function") { - if (typeof count !== "number") return tokenList.slice(0, sliceEnd); - return tokenList.slice(sliceEnd - count, sliceEnd); + if (typeof count !== "number") return tokenList.slice(0, sliceEnd) as Result; + return tokenList.slice(sliceEnd - count, sliceEnd) as Result; } const tokensBefore: TokenOrComment[] = []; @@ -728,7 +774,7 @@ export function getTokensBefore( if (filter(token)) tokensBefore.unshift(token); } } - return tokensBefore; + return tokensBefore as Result; } /** @@ -739,10 +785,14 @@ export function getTokensBefore( * If is a function, equivalent to `{ filter: fn }`. * @returns `Token`, or `null` if all were skipped. */ -export function getTokenAfter( +export function getTokenAfter( nodeOrToken: NodeOrToken, - skipOptions?: SkipOptions | number | FilterFn | null, -): TokenOrComment | null { + skipOptions?: Options, +): TokenResult | null { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult | null; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -754,7 +804,7 @@ export function getTokenAfter( ? skipOptions.skip : null; - const filter = + const filter: FilterFn | null | undefined = typeof skipOptions === "function" ? skipOptions : typeof skipOptions === "object" && skipOptions !== null @@ -796,19 +846,19 @@ export function getTokenAfter( const skipTo = startIndex + (skip ?? 0); // Avoid indexing out of bounds if (skipTo >= tokensLength) return null; - return tokenList[skipTo]; + return tokenList[skipTo] as Result; } if (typeof skip !== "number") { for (let i = startIndex; i < tokensLength; i++) { const token = tokenList[i]; - if (filter(token)) return token; + if (filter(token)) return token as Result; } } else { for (let i = startIndex; i < tokensLength; i++) { const token = tokenList[i]; if (filter(token)) { - if (skip <= 0) return token; + if (skip <= 0) return token as Result; skip--; } } @@ -844,10 +894,14 @@ export function getTokenOrCommentAfter( * If is a function, equivalent to `{ filter: fn }`. * @returns Array of `Token`s. */ -export function getTokensAfter( +export function getTokensAfter( nodeOrToken: NodeOrToken, - countOptions?: CountOptions | number | FilterFn | null, -): TokenOrComment[] { + countOptions?: Options, +): TokenResult[] { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult[]; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -858,7 +912,7 @@ export function getTokensAfter( ? countOptions.count : null; - const filter = + const filter: FilterFn | null | undefined = typeof countOptions === "function" ? countOptions : typeof countOptions === "object" && countOptions !== null @@ -894,8 +948,8 @@ export function getTokensAfter( // Fast path for the common case if (typeof filter !== "function") { - if (typeof count !== "number") return tokenList.slice(sliceStart); - return tokenList.slice(sliceStart, sliceStart + count); + if (typeof count !== "number") return tokenList.slice(sliceStart) as Result; + return tokenList.slice(sliceStart, sliceStart + count) as Result; } const tokenListAfter: TokenOrComment[] = []; @@ -910,7 +964,7 @@ export function getTokensAfter( if (filter(token)) tokenListAfter.push(token); } } - return tokenListAfter; + return tokenListAfter as Result; } /** @@ -927,11 +981,13 @@ export function getTokensAfter( * @param padding - Number of extra tokens on either side of center. * @returns Array of `Token`s between `left` and `right`. */ -export function getTokensBetween( - left: NodeOrToken, - right: NodeOrToken, - countOptions?: CountOptions | number | FilterFn | null, -): TokenOrComment[] { +export function getTokensBetween< + Options extends CountOptions | number | FilterFn | null | undefined, +>(left: NodeOrToken, right: NodeOrToken, countOptions?: Options): TokenResult[] { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult[]; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -940,7 +996,7 @@ export function getTokensBetween( const padding = typeof countOptions === "number" ? countOptions : 0; - const filter = + const filter: FilterFn | null | undefined = typeof countOptions === "function" ? countOptions : typeof countOptions === "object" && countOptions !== null @@ -996,8 +1052,8 @@ export function getTokensBetween( sliceEnd += padding; if (typeof filter !== "function") { - if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd); - return tokenList.slice(sliceStart, Math.min(sliceStart + count, sliceEnd)); + if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd) as Result; + return tokenList.slice(sliceStart, Math.min(sliceStart + count, sliceEnd)) as Result; } const tokensBetween: TokenOrComment[] = []; @@ -1012,7 +1068,7 @@ export function getTokensBetween( if (filter(token)) tokensBetween.push(token); } } - return tokensBetween; + return tokensBetween as Result; } /** @@ -1024,11 +1080,13 @@ export function getTokensBetween( * If is a function, equivalent to `{ filter: fn }`. * @returns `Token`, or `null` if all were skipped. */ -export function getFirstTokenBetween( - left: NodeOrToken, - right: NodeOrToken, - skipOptions?: SkipOptions | number | FilterFn | null, -): TokenOrComment | null { +export function getFirstTokenBetween< + Options extends SkipOptions | number | FilterFn | null | undefined, +>(left: NodeOrToken, right: NodeOrToken, skipOptions?: Options): TokenResult | null { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult | null; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -1040,7 +1098,7 @@ export function getFirstTokenBetween( ? skipOptions.skip : null; - const filter = + const filter: FilterFn | null | undefined = typeof skipOptions === "function" ? skipOptions : typeof skipOptions === "object" && skipOptions !== null @@ -1087,21 +1145,21 @@ export function getFirstTokenBetween( if (skipTo >= tokensLength) return null; const token = tokenList[skipTo]; if (token.start >= rangeEnd) return null; - return token; + return token as Result; } if (typeof skip !== "number") { for (let i = firstTokenIndex; i < tokensLength; i++) { const token = tokenList[i]; if (token.start >= rangeEnd) return null; - if (filter(token)) return token; + if (filter(token)) return token as Result; } } else { for (let i = firstTokenIndex; i < tokensLength; i++) { const token = tokenList[i]; if (token.start >= rangeEnd) return null; if (filter(token)) { - if (skip <= 0) return token; + if (skip <= 0) return token as Result; skip--; } } @@ -1119,11 +1177,13 @@ export function getFirstTokenBetween( * If is a function, equivalent to `{ filter: fn }`. * @returns Array of `Token`s between `left` and `right`. */ -export function getFirstTokensBetween( - left: NodeOrToken, - right: NodeOrToken, - countOptions?: CountOptions | number | FilterFn | null, -): TokenOrComment[] { +export function getFirstTokensBetween< + Options extends CountOptions | number | FilterFn | null | undefined, +>(left: NodeOrToken, right: NodeOrToken, countOptions?: Options): TokenResult[] { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult[]; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -1134,7 +1194,7 @@ export function getFirstTokensBetween( ? countOptions.count : null; - const filter = + const filter: FilterFn | null | undefined = typeof countOptions === "function" ? countOptions : typeof countOptions === "object" && countOptions !== null @@ -1187,8 +1247,8 @@ export function getFirstTokensBetween( } if (typeof filter !== "function") { - if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd); - return tokenList.slice(sliceStart, Math.min(sliceStart + count, sliceEnd)); + if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd) as Result; + return tokenList.slice(sliceStart, Math.min(sliceStart + count, sliceEnd)) as Result; } const firstTokens: TokenOrComment[] = []; @@ -1203,7 +1263,7 @@ export function getFirstTokensBetween( if (filter(token)) firstTokens.push(token); } } - return firstTokens; + return firstTokens as Result; } /** @@ -1215,11 +1275,13 @@ export function getFirstTokensBetween( * If is a function, equivalent to `{ filter: fn }`. * @returns `Token`, or `null` if all were skipped. */ -export function getLastTokenBetween( - left: NodeOrToken, - right: NodeOrToken, - skipOptions?: SkipOptions | number | FilterFn | null, -): TokenOrComment | null { +export function getLastTokenBetween< + Options extends SkipOptions | number | FilterFn | null | undefined, +>(left: NodeOrToken, right: NodeOrToken, skipOptions?: Options): TokenResult | null { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult | null; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -1231,7 +1293,7 @@ export function getLastTokenBetween( ? skipOptions.skip : null; - const filter = + const filter: FilterFn | null | undefined = typeof skipOptions === "function" ? skipOptions : typeof skipOptions === "object" && skipOptions !== null @@ -1279,21 +1341,21 @@ export function getLastTokenBetween( if (skipTo < 0) return null; const token = tokenList[skipTo]; if (token.start < rangeStart) return null; - return token; + return token as Result; } if (typeof skip !== "number") { for (let i = lastTokenIndex; i >= 0; i--) { const token = tokenList[i]; if (token.start < rangeStart) return null; - if (filter(token)) return token; + if (filter(token)) return token as Result; } } else { for (let i = lastTokenIndex; i >= 0; i--) { const token = tokenList[i]; if (token.start < rangeStart) return null; if (filter(token)) { - if (skip <= 0) return token; + if (skip <= 0) return token as Result; skip--; } } @@ -1311,11 +1373,13 @@ export function getLastTokenBetween( * If is a function, equivalent to `{ filter: fn }`. * @returns Array of `Token`s between `left` and `right`. */ -export function getLastTokensBetween( - left: NodeOrToken, - right: NodeOrToken, - countOptions?: CountOptions | number | FilterFn | null, -): TokenOrComment[] { +export function getLastTokensBetween< + Options extends CountOptions | number | FilterFn | null | undefined, +>(left: NodeOrToken, right: NodeOrToken, countOptions?: Options): TokenResult[] { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult[]; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -1326,7 +1390,7 @@ export function getLastTokensBetween( ? countOptions.count : null; - const filter = + const filter: FilterFn | null | undefined = typeof countOptions === "function" ? countOptions : typeof countOptions === "object" && countOptions !== null @@ -1379,8 +1443,8 @@ export function getLastTokensBetween( // Fast path for the common case if (typeof filter !== "function") { - if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd); - return tokenList.slice(Math.max(sliceStart, sliceEnd - count), sliceEnd); + if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd) as Result; + return tokenList.slice(Math.max(sliceStart, sliceEnd - count), sliceEnd) as Result; } const tokensBetween: TokenOrComment[] = []; @@ -1396,7 +1460,7 @@ export function getLastTokensBetween( if (filter(token)) tokensBetween.unshift(token); } } - return tokensBetween; + return tokensBetween as Result; } /** @@ -1405,10 +1469,14 @@ export function getLastTokensBetween( * @param rangeOptions - Options object. * @returns The token starting at index, or `null` if no such token. */ -export function getTokenByRangeStart( +export function getTokenByRangeStart( index: number, - rangeOptions?: RangeOptions | null, -): TokenOrComment | null { + rangeOptions?: Options, +): TokenResult | null { + // TypeScript cannot verify conditional return types within the function body, + // so we use `Result` alias + casts on return statements + type Result = TokenResult | null; + if (tokens === null) initTokens(); debugAssertIsNonNull(tokens); @@ -1432,7 +1500,7 @@ export function getTokenByRangeStart( const mid = (lo + hi) >> 1; const tokenStart = tokenList[mid].start; if (tokenStart === index) { - return tokenList[mid]; + return tokenList[mid] as Result; } else if (tokenStart < index) { lo = mid + 1; } else { diff --git a/apps/oxlint/test/tokens.test.ts b/apps/oxlint/test/tokens.test.ts index 0528057f6721a..719060c5e6cf1 100644 --- a/apps/oxlint/test/tokens.test.ts +++ b/apps/oxlint/test/tokens.test.ts @@ -31,7 +31,7 @@ import { parse as parseRaw } from "../src-js/package/parse.ts"; import { debugAssertIsNonNull } from "../src-js/utils/asserts.ts"; import type { Node } from "../src-js/plugins/types.ts"; -import type { Token } from "../src-js/plugins/tokens.ts"; +import type { TokenOrComment } from "../src-js/plugins/tokens.ts"; import type { BinaryExpression } from "../src-js/generated/types.d.ts"; // Source text used for most tests @@ -1301,7 +1301,7 @@ describe("when calling getFirstToken & getTokenAfter", () => { setup("(function(a, /*b,*/ c){})"); const tokens = []; // TODO: replace this verbatim range with `ast` - let token = getFirstToken({ range: [0, 25] } as Node); + let token: TokenOrComment | null = getFirstToken({ range: [0, 25] } as Node); while (token) { tokens.push(token); @@ -1329,7 +1329,7 @@ describe("when calling getFirstToken & getTokenAfter", () => { setup("(function(a,/*b,*/c){})"); const tokens = []; // TODO: replace this verbatim range with `ast` - let token = getFirstToken({ range: [0, 23] } as Node); + let token: TokenOrComment | null = getFirstToken({ range: [0, 23] } as Node); while (token) { tokens.push(token); @@ -1360,7 +1360,7 @@ describe("when calling getLastToken & getTokenBefore", () => { setup("(function(a, /*b,*/ c){})"); const tokens = []; // TODO: replace this verbatim range with `ast` - let token = getLastToken({ range: [0, 25] } as Node); + let token: TokenOrComment | null = getLastToken({ range: [0, 25] } as Node); while (token) { tokens.push(token); @@ -1388,7 +1388,7 @@ describe("when calling getLastToken & getTokenBefore", () => { setup("(function(a,/*b,*/c){})"); const tokens = []; // TODO: replace this verbatim range with `ast` - let token = getLastToken({ range: [0, 23] } as Node); + let token: TokenOrComment | null = getLastToken({ range: [0, 23] } as Node); while (token) { tokens.push(token); @@ -1561,7 +1561,7 @@ describe("token regex across sequential files", () => { // File 2: no regex tokens - reused token objects should have `regex: undefined` const withoutRegex = "var y = 1;"; setup(withoutRegex); - const tokens2 = getTokens({ range: [0, withoutRegex.length] } as Node) as Token[]; + const tokens2 = getTokens({ range: [0, withoutRegex.length] } as Node); for (const token of tokens2) { expect(token.regex).toBeUndefined(); } @@ -1571,7 +1571,7 @@ describe("token regex across sequential files", () => { // File 1: no regex const withoutRegex = "var x = 1;"; setup(withoutRegex); - const tokens1 = getTokens({ range: [0, withoutRegex.length] } as Node) as Token[]; + const tokens1 = getTokens({ range: [0, withoutRegex.length] } as Node); for (const token of tokens1) { expect(token.regex).toBeUndefined(); } @@ -1579,7 +1579,7 @@ describe("token regex across sequential files", () => { // File 2: has a regex token const withRegex = "var y = /foo/m;"; setup(withRegex); - const tokens2 = getTokens({ range: [0, withRegex.length] } as Node) as Token[]; + const tokens2 = getTokens({ range: [0, withRegex.length] } as Node); const regexToken2 = tokens2.find((t) => t.type === "RegularExpression"); expect(regexToken2).toBeDefined(); expect(regexToken2!.regex).toEqual({ pattern: "foo", flags: "m" }); @@ -1623,7 +1623,7 @@ describe("token regex across sequential files", () => { // File 2: no regex - all tokens should have `regex: undefined` const noRegex = "x + y + z;"; setup(noRegex); - const tokens2 = getTokens({ range: [0, noRegex.length] } as Node) as Token[]; + const tokens2 = getTokens({ range: [0, noRegex.length] } as Node); for (const token of tokens2) { expect(token.regex).toBeUndefined(); } diff --git a/apps/oxlint/test/tokens.type_test.ts b/apps/oxlint/test/tokens.type_test.ts new file mode 100644 index 0000000000000..0a988d3fd03e8 --- /dev/null +++ b/apps/oxlint/test/tokens.type_test.ts @@ -0,0 +1,446 @@ +/** + * Type tests for token method return types. + * + * These are compile-time only — they don't run any code, they just verify that the conditional return types + * resolve correctly for various calling patterns. + * + * Enforced by the type-checker: If any `satisfies` check fails, type check will error. + * + * `getTokens` and `getFirstToken` are tested exhaustively because they represent the two return type patterns: + * 1. Array - `TokenResult[]` + * 2. Single - `TokenResult | null` + * + * All other conditional-return methods use the same `TokenResult` type, so they only get minimal tests: + * 1. No options -> `Token` + * 2. `{ includeComments: true }` -> `TokenOrComment` + * This guards against a method accidentally missing the `TokenResult` return type. + */ + +import { + getFirstToken, + getFirstTokenBetween, + getFirstTokens, + getFirstTokensBetween, + getLastToken, + getLastTokenBetween, + getLastTokens, + getLastTokensBetween, + getTokenAfter, + getTokenBefore, + getTokenByRangeStart, + getTokens, + getTokensAfter, + getTokensBefore, + getTokensBetween, +} from "../src-js/plugins/tokens_methods.ts"; + +import type { Node } from "../src-js/plugins/types.ts"; +import type { Token, TokenOrComment } from "../src-js/plugins/tokens.ts"; +import type { CountOptions, SkipOptions } from "../src-js/plugins/tokens_methods.ts"; + +type IsExact = [T] extends [U] ? ([U] extends [T] ? true : false) : false; + +declare const node: Node; + +// --- `getTokens` --- + +// No options -> `Token[]` +{ + const result = getTokens(node); + true satisfies IsExact; +} + +// `null` options -> `Token[]` +{ + const result = getTokens(node, null); + true satisfies IsExact; +} + +// `undefined` options -> `Token[]` +{ + const result = getTokens(node, undefined); + true satisfies IsExact; +} + +// Empty options object -> `Token[]` +{ + const opts = {}; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// `{ includeComments: true }` -> `TokenOrComment[]` +{ + const result = getTokens(node, { includeComments: true }); + true satisfies IsExact; +} + +// `{ includeComments: false }` -> `Token[]` +{ + const result = getTokens(node, { includeComments: false }); + true satisfies IsExact; +} + +// Variable boolean (widened) -> `TokenOrComment[]` (conservative) +{ + const flag: boolean = true; + const result = getTokens(node, { includeComments: flag }); + true satisfies IsExact; +} + +// Options object assigned to variable (`true` widened to `boolean`) -> `TokenOrComment[]` +{ + const opts = { includeComments: true }; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Options object assigned to variable (`false` widened to `boolean`) -> `TokenOrComment[]` (conservative) +{ + const opts = { includeComments: false }; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Options object with `as const` (true) -> `TokenOrComment[]` +{ + const opts = { includeComments: true } as const; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Options object with `as const` (false) -> `Token[]` +{ + const opts = { includeComments: false } as const; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Options with `count` only -> `Token[]` +{ + const opts = { count: 3 }; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Options with `filter` only -> `Token[]` +{ + const opts = { filter: (token: Token) => token.type === "Keyword" }; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Options with `count` and `includeComments: true` -> `TokenOrComment[]` +{ + const opts = { count: 3, includeComments: true }; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Options with `count` and `includeComments: false` inline -> `Token[]` +{ + const result = getTokens(node, { count: 3, includeComments: false }); + true satisfies IsExact; +} + +// Options with `count` and `includeComments: false` -> `TokenOrComment[]` (conservative) +{ + const opts = { count: 3, includeComments: false }; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Options with `count` and `includeComments: true` with `as const` -> `TokenOrComment[]` +{ + const opts = { count: 3, includeComments: true } as const; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Options with `count` and `includeComments: false` with `as const` -> `Token[]` +{ + const opts = { count: 3, includeComments: false } as const; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Number option -> `Token[]` +{ + const result = getTokens(node, 5); + true satisfies IsExact; +} + +// Filter function -> `Token[]` +{ + const result = getTokens(node, (token) => token.type === "Keyword"); + true satisfies IsExact; +} + +// Variable typed as `CountOptions` -> `TokenOrComment[]` (conservative) +{ + const opts = {} as CountOptions; + const result = getTokens(node, opts); + true satisfies IsExact; +} + +// Variable typed as the full union -> should be `TokenOrComment[]` but resolves to `Token[]`. +// The union distributes `MayIncludeComments` to `true | false` = `boolean`, +// which doesn't extend `true`, so it falls to `Token[]`. +// This is a known limitation, but this calling pattern is unrealistic in practice. +{ + const opts = {} as CountOptions | number | null | undefined; + const result = getTokens(node, opts); + // @ts-expect-error — see above + true satisfies IsExact; +} + +// --- `getFirstToken` --- + +// No options -> `Token | null` +{ + const result = getFirstToken(node); + true satisfies IsExact; +} + +// `null` options -> `Token | null` +{ + const result = getFirstToken(node, null); + true satisfies IsExact; +} + +// `undefined` options -> `Token | null` +{ + const result = getFirstToken(node, undefined); + true satisfies IsExact; +} + +// Empty options object -> `Token | null` +{ + const opts = {}; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// `{ includeComments: true }` -> `TokenOrComment | null` +{ + const result = getFirstToken(node, { includeComments: true }); + true satisfies IsExact; +} + +// `{ includeComments: false }` -> `Token | null` +{ + const result = getFirstToken(node, { includeComments: false }); + true satisfies IsExact; +} + +// Variable boolean (widened) -> `TokenOrComment | null` (conservative) +{ + const flag: boolean = true; + const result = getFirstToken(node, { includeComments: flag }); + true satisfies IsExact; +} + +// Options object assigned to variable (`true` widened to `boolean`) -> `TokenOrComment | null` +{ + const opts = { includeComments: true }; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// Options object assigned to variable (`false` widened to `boolean`) -> `TokenOrComment | null` (conservative) +{ + const opts = { includeComments: false }; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// Options object with `as const` (true) -> `TokenOrComment | null` +{ + const opts = { includeComments: true } as const; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// Options object with `as const` (false) -> `Token | null` +{ + const opts = { includeComments: false } as const; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// Options with `skip` only -> `Token | null` +{ + const opts = { skip: 1 }; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// Options with `filter` only -> `Token | null` +{ + const opts = { filter: (token: Token) => token.type === "Keyword" }; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// Options with `skip` and `includeComments: true` -> `TokenOrComment | null` +{ + const opts = { skip: 1, includeComments: true }; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// Options with `skip` and `includeComments: false` inline -> `Token | null` +{ + const result = getFirstToken(node, { skip: 1, includeComments: false }); + true satisfies IsExact; +} + +// Options with `skip` and `includeComments: false` -> `TokenOrComment | null` (conservative) +{ + const opts = { skip: 1, includeComments: false }; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// Options with `skip` and `includeComments: true` with `as const` -> `TokenOrComment | null` +{ + const opts = { skip: 1, includeComments: true } as const; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// Options with `skip` and `includeComments: false` with `as const` -> `Token | null` +{ + const opts = { skip: 1, includeComments: false } as const; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// Number option -> `Token | null` +{ + const result = getFirstToken(node, 5); + true satisfies IsExact; +} + +// Filter function -> `Token | null` +{ + const result = getFirstToken(node, (token) => token.type === "Keyword"); + true satisfies IsExact; +} + +// Variable typed as `SkipOptions` -> `TokenOrComment | null` (conservative) +{ + const opts = {} as SkipOptions; + const result = getFirstToken(node, opts); + true satisfies IsExact; +} + +// --- Minimal tests for remaining methods --- + +// Each method gets two tests: +// 1. No options -> `Token` +// 2. `{ includeComments: true }` -> `TokenOrComment` + +// `getFirstTokens` +{ + const noOptions = getFirstTokens(node); + true satisfies IsExact; + const withOptions = getFirstTokens(node, { includeComments: true }); + true satisfies IsExact; +} + +// `getLastToken` +{ + const noOptions = getLastToken(node); + true satisfies IsExact; + const withOptions = getLastToken(node, { includeComments: true }); + true satisfies IsExact; +} + +// `getLastTokens` +{ + const noOptions = getLastTokens(node); + true satisfies IsExact; + const withOptions = getLastTokens(node, { includeComments: true }); + true satisfies IsExact; +} + +// `getTokenBefore` +{ + const noOptions = getTokenBefore(node); + true satisfies IsExact; + const withOptions = getTokenBefore(node, { includeComments: true }); + true satisfies IsExact; +} + +// `getTokenAfter` +{ + const noOptions = getTokenAfter(node); + true satisfies IsExact; + const withOptions = getTokenAfter(node, { includeComments: true }); + true satisfies IsExact; +} + +// `getTokensBefore` +{ + const noOptions = getTokensBefore(node); + true satisfies IsExact; + const withOptions = getTokensBefore(node, { includeComments: true }); + true satisfies IsExact; +} + +// `getTokensAfter` +{ + const noOptions = getTokensAfter(node); + true satisfies IsExact; + const withOptions = getTokensAfter(node, { includeComments: true }); + true satisfies IsExact; +} + +// `getTokensBetween` +{ + const noOptions = getTokensBetween(node, node); + true satisfies IsExact; + const withOptions = getTokensBetween(node, node, { includeComments: true }); + true satisfies IsExact; +} + +// `getFirstTokenBetween` +{ + const noOptions = getFirstTokenBetween(node, node); + true satisfies IsExact; + const withOptions = getFirstTokenBetween(node, node, { includeComments: true }); + true satisfies IsExact; +} + +// `getFirstTokensBetween` +{ + const noOptions = getFirstTokensBetween(node, node); + true satisfies IsExact; + const withOptions = getFirstTokensBetween(node, node, { includeComments: true }); + true satisfies IsExact; +} + +// `getLastTokenBetween` +{ + const noOptions = getLastTokenBetween(node, node); + true satisfies IsExact; + const withOptions = getLastTokenBetween(node, node, { includeComments: true }); + true satisfies IsExact; +} + +// `getLastTokensBetween` +{ + const noOptions = getLastTokensBetween(node, node); + true satisfies IsExact; + const withOptions = getLastTokensBetween(node, node, { includeComments: true }); + true satisfies IsExact; +} + +// `getTokenByRangeStart` +{ + const noOptions = getTokenByRangeStart(0); + true satisfies IsExact; + const withOptions = getTokenByRangeStart(0, { includeComments: true }); + true satisfies IsExact; +}