diff --git a/apps/oxlint/src-js/plugins/tokens.ts b/apps/oxlint/src-js/plugins/tokens.ts index 31bd9b310bf47..55845e4dc9ea9 100644 --- a/apps/oxlint/src-js/plugins/tokens.ts +++ b/apps/oxlint/src-js/plugins/tokens.ts @@ -397,9 +397,104 @@ export function getLastToken(node: Node, skipOptions?: SkipOptions | number | Fi * @param countOptions? - Options object. Same options as `getFirstTokens()`. * @returns Array of `Token`s. */ -// oxlint-disable-next-line no-unused-vars export function getLastTokens(node: Node, countOptions?: CountOptions | number | FilterFn | null): Token[] { - throw new Error('`sourceCode.getLastTokens` not implemented yet'); // TODO + if (tokens === null) initTokens(); + debugAssertIsNonNull(tokens); + debugAssertIsNonNull(comments); + + // 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 = + 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 { range } = node, + rangeStart = range[0], + rangeEnd = range[1]; + + // Binary search for first token within `node`'s range + const tokensLength = nodeTokens.length; + let sliceStart = tokensLength; + for (let lo = 0; lo < sliceStart; ) { + const mid = (lo + sliceStart) >> 1; + if (nodeTokens[mid].range[0] < rangeStart) { + lo = mid + 1; + } else { + sliceStart = mid; + } + } + + // Binary search for the first token outside `node`'s range + let sliceEnd = tokensLength; + for (let lo = sliceStart; lo < sliceEnd; ) { + const mid = (lo + sliceEnd) >> 1; + if (nodeTokens[mid].range[0] < rangeEnd) { + lo = mid + 1; + } else { + sliceEnd = mid; + } + } + + let lastTokens: Token[] = []; + if (typeof filter !== 'function') { + if (typeof count !== 'number') { + lastTokens = nodeTokens.slice(sliceStart, sliceEnd); + } else { + lastTokens = nodeTokens.slice(max(sliceStart, sliceEnd - count), sliceEnd); + } + } else { + if (typeof count !== 'number') { + lastTokens = []; + for (let i = sliceStart; i < sliceEnd; i++) { + const token = nodeTokens[i]; + if (filter(token)) { + lastTokens.push(token); + } + } + } else { + lastTokens = []; + // Count is the number of tokens within range from the end so we iterate in reverse + for (let i = sliceEnd - 1; i >= sliceStart; i--) { + const token = nodeTokens[i]; + if (filter(token)) { + lastTokens.unshift(token); + if (lastTokens.length === count) { + break; + } + } + } + } + } + + return lastTokens; } /** diff --git a/apps/oxlint/test/tokens.test.ts b/apps/oxlint/test/tokens.test.ts index 039fa8bb4fe65..995d13f3c6603 100644 --- a/apps/oxlint/test/tokens.test.ts +++ b/apps/oxlint/test/tokens.test.ts @@ -7,6 +7,17 @@ import { getTokensAfter, getTokenAfter, getFirstTokens, + getFirstToken, + getLastTokens, + getLastToken, + getFirstTokensBetween, + getFirstTokenBetween, + getLastTokenBetween, + getLastTokensBetween, + getTokenByRangeStart, + getTokensBetween, + getTokenOrCommentBefore, + getTokenOrCommentAfter, } from '../src-js/plugins/tokens.js'; import { resetSourceAndAst } from '../src-js/plugins/source_code.js'; import type { Node } from '../src-js/plugins/types.js'; @@ -585,64 +596,178 @@ describe('when calling getFirstTokens', () => { describe('when calling getFirstToken', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getFirstToken; }); +// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L851-L930 describe('when calling getLastTokens', () => { - /* oxlint-disable-next-line no-disabled-tests expect-expect */ - it('is to be implemented'); + it("should retrieve zero tokens from the end of a node's token stream", () => { + assert.deepStrictEqual( + getLastTokens(BinaryExpression, 0).map((token) => token.value), + [], + ); + }); + + it("should retrieve one token from the end of a node's token stream", () => { + assert.deepStrictEqual( + getLastTokens(BinaryExpression, 1).map((token) => token.value), + ['b'], + ); + }); + + it("should retrieve more than one token from the end of a node's token stream", () => { + assert.deepStrictEqual( + getLastTokens(BinaryExpression, 2).map((token) => token.value), + ['*', 'b'], + ); + }); + + it("should retrieve all tokens from the end of a node's token stream", () => { + assert.deepStrictEqual( + getLastTokens(BinaryExpression, 9e9).map((token) => token.value), + ['a', '*', 'b'], + ); + }); + + it("should retrieve more than one token from the end of a node's token stream with count option", () => { + assert.deepStrictEqual( + getLastTokens(BinaryExpression, { count: 2 }).map((token) => token.value), + ['*', 'b'], + ); + }); + + it("should retrieve matched tokens from the end of a node's token stream with filter option", () => { + assert.deepStrictEqual( + getLastTokens(BinaryExpression, (t) => t.type === 'Identifier').map((token) => token.value), + ['a', 'b'], + ); + assert.deepStrictEqual( + getLastTokens(BinaryExpression, { + filter: (t) => t.type === 'Identifier', + }).map((token) => token.value), + ['a', 'b'], + ); + }); + + it("should retrieve matched tokens from the end of a node's token stream with filter and count options", () => { + assert.deepStrictEqual( + getLastTokens(BinaryExpression, { + count: 1, + filter: (t) => t.type === 'Identifier', + }).map((token) => token.value), + ['b'], + ); + }); + + it("should retrieve all tokens from the end of a node's token stream with includeComments option", () => { + assert.deepStrictEqual( + getLastTokens(BinaryExpression, { + includeComments: true, + }).map((token) => token.value), + ['a', 'D', '*', 'b'], + ); + }); + + it("should retrieve matched tokens from the end of a node's token stream with includeComments and count options", () => { + assert.deepStrictEqual( + getLastTokens(BinaryExpression, { + includeComments: true, + count: 3, + }).map((token) => token.value), + ['D', '*', 'b'], + ); + }); + + it("should retrieve matched tokens from the end of a node's token stream with includeComments and count and filter options", () => { + assert.deepStrictEqual( + getLastTokens(BinaryExpression, { + includeComments: true, + count: 3, + filter: (t) => t.type !== 'Punctuator', + }).map((token) => token.value), + ['a', 'D', 'b'], + ); + }); }); describe('when calling getLastToken', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getLastToken; }); describe('when calling getFirstTokensBetween', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getFirstTokensBetween; }); describe('when calling getFirstTokenBetween', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getFirstTokenBetween; }); describe('when calling getLastTokensBetween', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getLastTokensBetween; }); describe('when calling getLastTokenBetween', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getLastTokenBetween; }); describe('when calling getTokensBetween', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getTokensBetween; }); describe('when calling getTokenByRangeStart', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getTokenByRangeStart; }); describe('when calling getTokenOrCommentBefore', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getTokenOrCommentBefore; }); describe('when calling getTokenOrCommentAfter', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getTokenOrCommentAfter; }); describe('when calling getFirstToken & getTokenAfter', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getFirstToken; + /* oxlint-disable-next-line no-unused-expressions */ + getTokenAfter; }); describe('when calling getLastToken & getTokenBefore', () => { /* oxlint-disable-next-line no-disabled-tests expect-expect */ it('is to be implemented'); + /* oxlint-disable-next-line no-unused-expressions */ + getLastToken; + /* oxlint-disable-next-line no-unused-expressions */ + getTokenBefore; });