diff --git a/apps/oxlint/src-js/plugins/tokens.ts b/apps/oxlint/src-js/plugins/tokens.ts index e4678299e87b1..af22ec78fb7d3 100644 --- a/apps/oxlint/src-js/plugins/tokens.ts +++ b/apps/oxlint/src-js/plugins/tokens.ts @@ -527,14 +527,90 @@ export function getTokensBefore( * @param skipOptions? - Options object. Same options as `getFirstToken()`. * @returns `Token`, or `null` if all were skipped. */ -/* oxlint-disable no-unused-vars */ export function getTokenAfter( nodeOrToken: NodeOrToken | Comment, skipOptions?: SkipOptions | number | FilterFn | null, ): Token | null { - throw new Error('`sourceCode.getTokenAfter` 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 ?? 0) + : 0; + + 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; + + // 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 } = nodeOrToken, + rangeEnd = range[1]; + + // Binary search for the first token that starts at or after the end of the node/token + const tokensLength = nodeTokens.length; + let startIndex = tokensLength; + for (let lo = 0; lo < startIndex; ) { + const mid = (lo + startIndex) >> 1; + if (nodeTokens[mid].range[0] < rangeEnd) { + lo = mid + 1; + } else { + startIndex = mid; + } + } + + // Fast path for the common case + if (typeof filter !== 'function') { + if (typeof skip !== 'number') { + return nodeTokens[startIndex] ?? null; + } else { + return nodeTokens[startIndex + skip] ?? null; + } + } else { + if (typeof skip !== 'number') { + for (let i = startIndex; i < tokensLength; i++) { + const token = nodeTokens[i]; + if (filter(token)) { + return token; + } + } + } else { + for (let i = startIndex; i < tokensLength; i++) { + const token = nodeTokens[i]; + if (filter(token)) { + if (skip === 0) { + return token; + } + skip--; + } + } + } + } + + return null; } -/* 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 565bb35688616..e90369d2a5187 100644 --- a/apps/oxlint/test/tokens.test.ts +++ b/apps/oxlint/test/tokens.test.ts @@ -303,11 +303,105 @@ describe('when calling getTokenBefore', () => { }); }); +// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L461 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; + it('should retrieve one token after a node', () => { + assert.strictEqual(getTokenAfter(VariableDeclaratorIdentifier)!.value, '='); + }); + + it('should skip a given number of tokens', () => { + assert.strictEqual(getTokenAfter(VariableDeclaratorIdentifier, 1)!.value, 'a'); + assert.strictEqual(getTokenAfter(VariableDeclaratorIdentifier, 2)!.value, '*'); + }); + + it('should skip a given number of tokens with skip option', () => { + assert.strictEqual(getTokenAfter(VariableDeclaratorIdentifier, { skip: 1 })!.value, 'a'); + assert.strictEqual(getTokenAfter(VariableDeclaratorIdentifier, { skip: 2 })!.value, '*'); + }); + + it('should retrieve matched token with filter option', () => { + assert.strictEqual(getTokenAfter(VariableDeclaratorIdentifier, (t) => t.type === 'Identifier')!.value, 'a'); + assert.strictEqual( + getTokenAfter(VariableDeclaratorIdentifier, { + filter: (t) => t.type === 'Identifier', + })!.value, + 'a', + ); + }); + + it('should retrieve matched token with filter and skip options', () => { + assert.strictEqual( + getTokenAfter(VariableDeclaratorIdentifier, { + skip: 1, + filter: (t) => t.type === 'Identifier', + })!.value, + 'b', + ); + }); + + it('should retrieve one token or comment after a node with includeComments option', () => { + assert.strictEqual( + getTokenAfter(VariableDeclaratorIdentifier, { + includeComments: true, + })!.value, + 'B', + ); + }); + + it('should retrieve one token or comment after a node with includeComments and skip options', () => { + assert.strictEqual( + getTokenAfter(VariableDeclaratorIdentifier, { + includeComments: true, + skip: 2, + })!.value, + 'C', + ); + }); + + it('should retrieve one token or comment after a node with includeComments and skip and filter options', () => { + assert.strictEqual( + getTokenAfter(VariableDeclaratorIdentifier, { + includeComments: true, + skip: 2, + filter: (t) => t.type.startsWith('Block'), + })!.value, + 'D', + ); + }); + + it('should retrieve the next node if the comment at the first of source code is specified.', () => { + resetSourceAndAst(); + sourceText = '/*comment*/ a + b'; + // TODO: replace this verbatim range with `ast.comments[0]` + const token = getTokenAfter({ range: [0, 12] } as Node)!; + + assert.strictEqual(token.value, 'a'); + resetSourceAndAst(); + }); + + it('should retrieve the next comment if the last token is specified.', () => { + resetSourceAndAst(); + sourceText = 'a + b /*comment*/'; + // TODO: replace this verbatim range with `ast.tokens[2]` + const token = getTokenAfter({ range: [4, 5] } as Node, { + includeComments: true, + }); + + assert.strictEqual(token!.value, 'comment'); + resetSourceAndAst(); + }); + + it('should retrieve null if the last comment is specified.', () => { + resetSourceAndAst(); + sourceText = 'a + b /*comment*/'; + // TODO: replace this verbatim range with `ast.comments[0]` + const token = getTokenAfter({ range: [6, 17] } as Node, { + includeComments: true, + }); + + assert.strictEqual(token, null); + resetSourceAndAst(); + }); }); // https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L363-L459