diff --git a/apps/oxlint/src-js/plugins/tokens.ts b/apps/oxlint/src-js/plugins/tokens.ts index a7a683981fe5d..d969c203ba59b 100644 --- a/apps/oxlint/src-js/plugins/tokens.ts +++ b/apps/oxlint/src-js/plugins/tokens.ts @@ -1162,22 +1162,110 @@ export function getFirstTokenBetween( /** * Get the first tokens between two non-overlapping nodes. - * @param nodeOrToken1 - Node before the desired token range. - * @param nodeOrToken2 - Node after the desired token range. + * @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 between `nodeOrToken1` and `nodeOrToken2`. + * @returns Array of `Token`s between `left` and `right`. */ -/* oxlint-disable no-unused-vars */ export function getFirstTokensBetween( - nodeOrToken1: NodeOrToken | Comment, - nodeOrToken2: NodeOrToken | Comment, + left: NodeOrToken | Comment, + right: NodeOrToken | Comment, countOptions?: CountOptions | number | FilterFn | null, ): Token[] { - throw new Error('`sourceCode.getFirstTokensBetween` not implemented yet'); // TODO + if (tokens === null) initTokens(); + debugAssertIsNonNull(tokens); + debugAssertIsNonNull(comments); + + const count = + typeof countOptions === 'number' + ? countOptions + : typeof countOptions === 'object' && countOptions !== null + ? countOptions.count + : null; + + const filter = + typeof countOptions === 'function' + ? countOptions + : typeof countOptions === 'object' && countOptions !== null + ? countOptions.filter + : null; + + const includeComments = + typeof countOptions === 'object' && + countOptions !== null && + 'includeComments' in countOptions && + countOptions.includeComments; + + let nodeTokens: Token[] | null = null; + if (includeComments) { + if (tokensWithComments === null) initTokensWithComments(); + debugAssertIsNonNull(tokensWithComments); + nodeTokens = tokensWithComments; + } else { + nodeTokens = tokens; + } + + // 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]; + + const tokensLength = nodeTokens.length; + + // Find the first token after `left` + 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; + } + } + + // Find the first token at or after `right` + 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 firstTokens: Token[]; + if (typeof filter !== 'function') { + if (typeof count !== 'number') { + firstTokens = nodeTokens.slice(sliceStart, sliceEnd); + } else { + firstTokens = nodeTokens.slice(sliceStart, min(sliceStart + count, sliceEnd)); + } + } else { + if (typeof count !== 'number') { + firstTokens = []; + for (let i = sliceStart; i < sliceEnd; i++) { + const token = nodeTokens[i]; + if (filter(token)) { + firstTokens.push(token); + } + } + } else { + firstTokens = []; + for (let i = sliceStart; i < sliceEnd && firstTokens.length < count; i++) { + const token = nodeTokens[i]; + if (filter(token)) { + firstTokens.push(token); + } + } + } + } + + return firstTokens; } -/* oxlint-enable no-unused-vars */ /** * Get the last token between two non-overlapping nodes. diff --git a/apps/oxlint/test/tokens.test.ts b/apps/oxlint/test/tokens.test.ts index 75f468ab59794..ac127dfdd728e 100644 --- a/apps/oxlint/test/tokens.test.ts +++ b/apps/oxlint/test/tokens.test.ts @@ -900,11 +900,62 @@ describe('when calling getLastTokenBetween', () => { }); }); +// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L1107-L1191 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; + it('should retrieve zero tokens between adjacent nodes', () => { + expect(getFirstTokensBetween(BinaryExpression, CallExpression).map((token) => token.value)).toEqual([]); + }); + + it('should retrieve multiple tokens between non-adjacent nodes with count option', () => { + expect( + getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, 2).map((token) => token.value), + ).toEqual(['=', 'a']); + expect( + getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { count: 2 }).map( + (token) => token.value, + ), + ).toEqual(['=', 'a']); + }); + + it('should retrieve matched tokens between non-adjacent nodes with filter option', () => { + expect( + getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { + filter: (t) => t.type !== 'Punctuator', + }).map((token) => token.value), + ).toEqual(['a']); + }); + + it('should retrieve all tokens between non-adjacent nodes with empty object option', () => { + expect( + getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {}).map((token) => token.value), + ).toEqual(['=', 'a', '*']); + }); + + it('should retrieve multiple tokens between non-adjacent nodes with includeComments option', () => { + expect( + getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { includeComments: true }).map( + (token) => token.value, + ), + ).toEqual(['B', '=', 'C', 'a', 'D', '*']); + }); + + it('should retrieve multiple tokens between non-adjacent nodes with includeComments and count options', () => { + expect( + getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { + includeComments: true, + count: 3, + }).map((token) => token.value), + ).toEqual(['B', '=', 'C']); + }); + + it('should retrieve multiple tokens and comments between non-adjacent nodes with includeComments and filter options', () => { + expect( + getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { + includeComments: true, + filter: (t) => t.type !== 'Punctuator', + }).map((token) => token.value), + ).toEqual(['B', 'C', 'a', 'D']); + }); }); describe('when calling getFirstTokenBetween', () => {