diff --git a/apps/oxlint/src-js/plugins/tokens.ts b/apps/oxlint/src-js/plugins/tokens.ts index 14776534c3608..59bf8961ec278 100644 --- a/apps/oxlint/src-js/plugins/tokens.ts +++ b/apps/oxlint/src-js/plugins/tokens.ts @@ -352,14 +352,95 @@ export function getTokenOrCommentBefore(nodeOrToken: NodeOrToken | Comment, skip * @param countOptions? - Options object. Same options as `getFirstTokens()`. * @returns Array of `Token`s. */ -/* oxlint-disable no-unused-vars */ export function getTokensBefore( nodeOrToken: NodeOrToken | Comment, countOptions?: CountOptions | number | FilterFn | null, ): Token[] { - throw new Error('`sourceCode.getTokensBefore` not implemented yet'); // TODO + if (tokens === null) initTokens(); + debugAssertIsNonNull(tokens); + debugAssertIsNonNull(comments); + + // Maximum number of tokens to return + const count = + typeof countOptions === 'number' + ? max(0, countOptions) + : typeof countOptions === 'object' && countOptions !== null + ? countOptions.count + : null; + + // Function to filter tokens + const filter = + typeof countOptions === 'function' + ? countOptions + : typeof countOptions === 'object' && countOptions !== null + ? countOptions.filter + : null; + + // Whether to return comment tokens + const includeComments = + typeof countOptions === 'object' && + countOptions !== null && + 'includeComments' in countOptions && + countOptions.includeComments; + + // Source array of tokens to search in + let nodeTokens: Token[] | null = null; + if (includeComments) { + if (tokensWithComments === null) { + tokensWithComments = [...tokens, ...comments].sort((a, b) => a.range[0] - b.range[0]); + } + nodeTokens = tokensWithComments; + } else { + nodeTokens = tokens; + } + + const targetStart = nodeOrToken.range[0]; + + let sliceEnd = 0; + let hi = nodeTokens.length; + while (sliceEnd < hi) { + const mid = (sliceEnd + hi) >> 1; + if (nodeTokens[mid].range[0] < targetStart) { + sliceEnd = mid + 1; + } else { + hi = mid; + } + } + + let tokensBefore: Token[]; + // Fast path for the common case + if (typeof filter !== 'function') { + if (typeof count !== 'number') { + tokensBefore = nodeTokens.slice(0, sliceEnd); + } else { + tokensBefore = nodeTokens.slice(sliceEnd - count, sliceEnd); + } + } else { + if (typeof count !== 'number') { + tokensBefore = []; + for (let i = 0; i < sliceEnd; i++) { + const token = nodeTokens[i]; + if (filter(token)) { + tokensBefore.push(token); + } + } + } else { + tokensBefore = []; + // Count is the number of preceding tokens so we iterate in reverse + for (let i = sliceEnd - 1; i >= 0; i--) { + const token = nodeTokens[i]; + if (filter(token)) { + tokensBefore.unshift(token); + } + if (tokensBefore.length === count) { + break; + } + } + } + } + + return tokensBefore; } -/* oxlint-enable no-unused-vars */ /** * Get the token that follows a given node or token. diff --git a/apps/oxlint/test/tokens.test.ts b/apps/oxlint/test/tokens.test.ts index 52cf7b498a4a7..3d5e2e1901c5f 100644 --- a/apps/oxlint/test/tokens.test.ts +++ b/apps/oxlint/test/tokens.test.ts @@ -1,30 +1,40 @@ import assert from 'node:assert'; import { describe, it, vi } from 'vitest'; -import { getTokens } from '../src-js/plugins/tokens.js'; +import { + getTokens, + getTokensBefore, + getTokenBefore, + getTokensAfter, + getTokenAfter, + getFirstTokens, +} from '../src-js/plugins/tokens.js'; +import { resetSourceAndAst } from '../src-js/plugins/source_code.js'; import type { Node } from '../src-js/plugins/types.js'; -let sourceText = 'null;'; +let sourceText = '/*A*/var answer/*B*/=/*C*/a/*D*/* b/*E*///F\n call();\n/*Z*/'; -vi.mock('../src-js/plugins/source_code.ts', () => { +vi.mock('../src-js/plugins/source_code.ts', async (importOriginal) => { + const original: any = await importOriginal(); return { + ...original, get sourceText() { return sourceText; }, }; }); +// TODO: We are lying about `Program`'s range here. +// The range provided by `@typescript-eslint/typescript-estree` does not match the assertions for that of `espree`. +// The deviation is being corrected in upcoming releases of ESLint and TS-ESLint. +// https://eslint.org/blog/2025/10/whats-coming-in-eslint-10.0.0/#updates-to-program-ast-node-range-coverage +// https://github.com/typescript-eslint/typescript-eslint/issues/11026#issuecomment-3421887632 +const Program = { range: [5, 55] } as Node; +const BinaryExpression = { range: [26, 35] } as Node; +/* oxlint-disable-next-line no-unused-vars */ +const VariableDeclaratorIdentifier = { range: [9, 15] } as Node; + // https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L62 describe('when calling getTokens', () => { - sourceText = '/*A*/var answer/*B*/=/*C*/a/*D*/* b/*E*///F\n call();\n/*Z*/'; - - // TODO: We are lying about `Program`'s range here. - // The range provided by `@typescript-eslint/typescript-estree` does not match the assertions for that of `espree`. - // The deviation is being corrected in upcoming releases of ESLint and TS-ESLint. - // https://eslint.org/blog/2025/10/whats-coming-in-eslint-10.0.0/#updates-to-program-ast-node-range-coverage - // https://github.com/typescript-eslint/typescript-eslint/issues/11026#issuecomment-3421887632 - const Program = { range: [5, 55] } as Node; - const BinaryExpression = { range: [26, 35] } as Node; - it('should retrieve all tokens for root node', () => { assert.deepStrictEqual( getTokens(Program).map((token) => token.value), @@ -104,3 +114,191 @@ describe('when calling getTokens', () => { ); }); }); + +// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L157 +describe('when calling getTokensBefore', () => { + it('should retrieve zero tokens before a node', () => { + assert.deepStrictEqual( + getTokensBefore(BinaryExpression, 0).map((token) => token.value), + [], + ); + }); + + it('should retrieve one token before a node', () => { + assert.deepStrictEqual( + getTokensBefore(BinaryExpression, 1).map((token) => token.value), + ['='], + ); + }); + + it('should retrieve more than one token before a node', () => { + assert.deepStrictEqual( + getTokensBefore(BinaryExpression, 2).map((token) => token.value), + ['answer', '='], + ); + }); + + it('should retrieve all tokens before a node', () => { + assert.deepStrictEqual( + getTokensBefore(BinaryExpression, 9e9).map((token) => token.value), + ['var', 'answer', '='], + ); + }); + + it('should retrieve more than one token before a node with count option', () => { + assert.deepStrictEqual( + getTokensBefore(BinaryExpression, { count: 2 }).map((token) => token.value), + ['answer', '='], + ); + }); + + it('should retrieve matched tokens before a node with count and filter options', () => { + assert.deepStrictEqual( + getTokensBefore(BinaryExpression, { + count: 1, + filter: (t) => t.value !== '=', + }).map((token) => token.value), + ['answer'], + ); + }); + + it('should retrieve all matched tokens before a node with filter option', () => { + assert.deepStrictEqual( + getTokensBefore(BinaryExpression, { + filter: (t) => t.value !== 'answer', + }).map((token) => token.value), + ['var', '='], + ); + }); + + it('should retrieve no tokens before the root node', () => { + assert.deepStrictEqual( + getTokensBefore(Program, { count: 1 }).map((token) => token.value), + [], + ); + }); + + it('should retrieve tokens and comments before a node with count and includeComments option', () => { + assert.deepStrictEqual( + getTokensBefore(BinaryExpression, { + count: 3, + includeComments: true, + }).map((token) => token.value), + ['B', '=', 'C'], + ); + }); + + it('should retrieve all tokens and comments before a node with includeComments option only', () => { + assert.deepStrictEqual( + getTokensBefore(BinaryExpression, { + includeComments: true, + }).map((token) => token.value), + ['A', 'var', 'answer', 'B', '=', 'C'], + ); + }); + + it('should retrieve all tokens and comments before a node with includeComments and filter options', () => { + assert.deepStrictEqual( + getTokensBefore(BinaryExpression, { + includeComments: true, + filter: (t) => t.type.startsWith('Block'), + }).map((token) => token.value), + ['A', 'B', 'C'], + ); + }); +}); + +describe('when calling getTokenBefore', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getTokenBefore; + /* oxlint-disable-next-line no-unused-expressions */ + resetSourceAndAst; +}); + +describe('when calling getTokenAfter', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getTokenAfter; +}); + +describe('when calling getTokensAfter', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getTokensAfter; +}); + +describe('when calling getFirstTokens', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getFirstTokens; +}); + +describe('when calling getFirstToken', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getLastTokens', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getLastToken', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getFirstTokensBetween', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getFirstTokenBetween', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getLastTokensBetween', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getLastTokenBetween', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getTokensBetween', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getTokenByRangeStart', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getTokenOrCommentBefore', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getTokenOrCommentAfter', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getFirstToken & getTokenAfter', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +}); + +describe('when calling getLastToken & getTokenBefore', () => { + /* oxlint-disable-next-line no-disabled-tests expect-expect */ + it('is to be implemented'); +});