From d160cfda0dda71fcd99b80f3edef8db2fa883614 Mon Sep 17 00:00:00 2001 From: Arsh <69170106+lilnasy@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:54:00 +0530 Subject: [PATCH] feat(linter/plugins): implement `SourceCode#getFirstTokenBetween()` --- apps/oxlint/src-js/plugins/tokens.ts | 97 +++++++++++++++++++++++++--- apps/oxlint/test/tokens.test.ts | 53 +++++++++++++-- 2 files changed, 137 insertions(+), 13 deletions(-) diff --git a/apps/oxlint/src-js/plugins/tokens.ts b/apps/oxlint/src-js/plugins/tokens.ts index 7158dcb2e955f..e35dc9fd612d7 100644 --- a/apps/oxlint/src-js/plugins/tokens.ts +++ b/apps/oxlint/src-js/plugins/tokens.ts @@ -1129,24 +1129,103 @@ export function getTokensBetween( /** * Get the first token 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 skipOptions? - Options object. * If is a number, equivalent to `{ skip: n }`. * If is a function, equivalent to `{ filter: fn }`. * @returns `Token`, or `null` if all were skipped. */ -/* oxlint-disable no-unused-vars */ export function getFirstTokenBetween( - nodeOrToken1: NodeOrToken | Comment, - nodeOrToken2: NodeOrToken | Comment, + left: NodeOrToken | Comment, + right: NodeOrToken | Comment, skipOptions?: SkipOptions | number | FilterFn | null, ): Token | null { - // TODO: Check that `skipOptions` being a number or a function is supported by ESLint in this method. - // Original type def was `SkipOptions | null`. I (@overlookmotel) assume that was a mistake. - throw new Error('`sourceCode.getFirstTokenBetween` not implemented yet'); // TODO + if (tokens === null) initTokens(); + debugAssertIsNonNull(tokens); + debugAssertIsNonNull(comments); + + let skip = + typeof skipOptions === 'number' + ? skipOptions + : typeof skipOptions === 'object' && skipOptions !== null + ? skipOptions.skip + : null; + + const filter = + typeof skipOptions === 'function' + ? skipOptions + : typeof skipOptions === 'object' && skipOptions !== null + ? skipOptions.filter + : null; + + const includeComments = + typeof skipOptions === 'object' && + skipOptions !== null && + 'includeComments' in skipOptions && + skipOptions.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; + + // Binary search for the token following the left node + let firstTokenIndex = tokensLength; + for (let lo = 0; lo < firstTokenIndex; ) { + const mid = (lo + firstTokenIndex) >> 1; + if (nodeTokens[mid].range[0] < rangeStart) { + lo = mid + 1; + } else { + firstTokenIndex = mid; + } + } + + if (typeof filter !== 'function') { + const token = nodeTokens[typeof skip === 'number' ? firstTokenIndex + skip : firstTokenIndex]; + if (token === undefined || token.range[0] >= rangeEnd) return null; + return token; + } else { + if (typeof skip !== 'number') { + for (let i = firstTokenIndex; i < tokensLength; i++) { + const token = nodeTokens[i]; + if (token.range[0] >= rangeEnd) { + break; + } + if (filter(token)) { + return token; + } + } + } else { + for (let i = firstTokenIndex; i < tokensLength; i++) { + const token = nodeTokens[i]; + if (token.range[0] >= rangeEnd) { + break; + } + if (filter(token)) { + if (skip <= 0) { + return token; + } + skip--; + } + } + } + } + + return null; } -/* oxlint-enable no-unused-vars */ /** * Get the first tokens between two non-overlapping nodes. diff --git a/apps/oxlint/test/tokens.test.ts b/apps/oxlint/test/tokens.test.ts index 382571e9676a0..8a44d1906542c 100644 --- a/apps/oxlint/test/tokens.test.ts +++ b/apps/oxlint/test/tokens.test.ts @@ -994,11 +994,56 @@ describe('when calling getFirstTokensBetween', () => { }); }); +// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L1193-L1296 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; + it('should return null between adjacent nodes', () => { + expect(getFirstTokenBetween(BinaryExpression, CallExpression)).toBeNull(); + }); + + it('should retrieve one token between non-adjacent nodes', () => { + expect(getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right)!.value).toEqual('='); + }); + + it('should retrieve one token between non-adjacent nodes with skip option', () => { + expect(getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, 1)!.value).toEqual('a'); + expect(getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { skip: 2 })!.value).toEqual('*'); + }); + + it("should return null if it's skipped beyond the right token", () => { + expect(getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { skip: 3 })).toBeNull(); + expect(getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { skip: 4 })).toBeNull(); + }); + + it('should retrieve the first matched token between non-adjacent nodes with filter option', () => { + expect( + getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { + filter: (t) => t.type !== 'Identifier', + })!.value, + ).toEqual('='); + }); + + it('should retrieve first token or comment between non-adjacent nodes with includeComments option', () => { + expect( + getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { includeComments: true })!.value, + ).toEqual('B'); + }); + + it('should retrieve first token or comment between non-adjacent nodes with includeComments and skip options', () => { + expect( + getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { includeComments: true, skip: 1 })! + .value, + ).toEqual('='); + }); + + it('should retrieve first token or comment between non-adjacent nodes with includeComments and skip and filter options', () => { + expect( + getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { + includeComments: true, + skip: 1, + filter: (t) => t.type !== 'Punctuator', + })!.value, + ).toEqual('C'); + }); }); describe('when calling getLastTokensBetween', () => {