Skip to content

Commit b96eab6

Browse files
committed
feat(linter/plugins): implement SourceCode#getTokensBetween()
1 parent bf9f469 commit b96eab6

File tree

2 files changed

+125
-16
lines changed

2 files changed

+125
-16
lines changed

apps/oxlint/src-js/plugins/tokens.ts

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,27 +1019,112 @@ export function getTokensAfter(
10191019

10201020
/**
10211021
* Get all of the tokens between two non-overlapping nodes.
1022-
* @param nodeOrToken1 - Node before the desired token range.
1023-
* @param nodeOrToken2 - Node after the desired token range.
1022+
* @param left - Node before the desired token range.
1023+
* @param right - Node after the desired token range.
10241024
* @param countOptions? - Options object. If this is a function then it's `options.filter`.
1025-
* @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`.
1025+
* @returns Array of `Token`s between `left` and `right`.
10261026
*/
10271027
/**
10281028
* Get all of the tokens between two non-overlapping nodes.
1029-
* @param nodeOrToken1 - Node before the desired token range.
1030-
* @param nodeOrToken2 - Node after the desired token range.
1029+
* @param left - Node before the desired token range.
1030+
* @param right - Node after the desired token range.
10311031
* @param padding - Number of extra tokens on either side of center.
1032-
* @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`.
1032+
* @returns Array of `Token`s between `left` and `right`.
10331033
*/
1034-
/* oxlint-disable no-unused-vars */
10351034
export function getTokensBetween(
1036-
nodeOrToken1: NodeOrToken | Comment,
1037-
nodeOrToken2: NodeOrToken | Comment,
1035+
left: NodeOrToken | Comment,
1036+
right: NodeOrToken | Comment,
10381037
countOptions?: CountOptions | number | FilterFn | null,
10391038
): Token[] {
1040-
throw new Error('`sourceCode.getTokensBetween` not implemented yet'); // TODO
1039+
if (tokens === null) initTokens();
1040+
debugAssertIsNonNull(tokens);
1041+
debugAssertIsNonNull(comments);
1042+
1043+
const count = typeof countOptions === 'object' && countOptions !== null ? countOptions.count : null;
1044+
1045+
const padding = typeof countOptions === 'number' ? countOptions : 0;
1046+
1047+
const filter =
1048+
typeof countOptions === 'function'
1049+
? countOptions
1050+
: typeof countOptions === 'object' && countOptions !== null
1051+
? countOptions.filter
1052+
: null;
1053+
1054+
const includeComments =
1055+
typeof countOptions === 'object' &&
1056+
countOptions !== null &&
1057+
'includeComments' in countOptions &&
1058+
countOptions.includeComments;
1059+
1060+
let nodeTokens: Token[];
1061+
if (includeComments) {
1062+
if (tokensWithComments === null) initTokensWithComments();
1063+
debugAssertIsNonNull(tokensWithComments);
1064+
nodeTokens = tokensWithComments;
1065+
} else {
1066+
nodeTokens = tokens;
1067+
}
1068+
1069+
const rangeStart = left.range[1],
1070+
rangeEnd = right.range[0],
1071+
tokensLength = nodeTokens.length;
1072+
1073+
// Binary search for first token past the beginning of the `between` range
1074+
let sliceStart = tokensLength;
1075+
for (let lo = 0; lo < sliceStart; ) {
1076+
const mid = (lo + sliceStart) >> 1;
1077+
if (nodeTokens[mid].range[0] < rangeStart) {
1078+
lo = mid + 1;
1079+
} else {
1080+
sliceStart = mid;
1081+
}
1082+
}
1083+
1084+
// Binary search for first token past the end of the `between` range
1085+
let sliceEnd = tokensLength;
1086+
for (let lo = sliceStart; lo < sliceEnd; ) {
1087+
const mid = (lo + sliceEnd) >> 1;
1088+
if (nodeTokens[mid].range[0] < rangeEnd) {
1089+
lo = mid + 1;
1090+
} else {
1091+
sliceEnd = mid;
1092+
}
1093+
}
1094+
1095+
// Apply padding
1096+
sliceStart = max(0, sliceStart - padding);
1097+
sliceEnd += padding;
1098+
1099+
let tokensBetween: Token[];
1100+
if (typeof filter !== 'function') {
1101+
if (typeof count !== 'number') {
1102+
tokensBetween = nodeTokens.slice(sliceStart, sliceEnd);
1103+
} else {
1104+
tokensBetween = nodeTokens.slice(sliceStart, min(sliceStart + count, sliceEnd));
1105+
}
1106+
} else {
1107+
if (typeof count !== 'number') {
1108+
tokensBetween = [];
1109+
for (let i = sliceStart; i < sliceEnd; i++) {
1110+
const token = nodeTokens[i];
1111+
if (filter(token)) {
1112+
tokensBetween.push(token);
1113+
}
1114+
}
1115+
} else {
1116+
tokensBetween = [];
1117+
for (let i = sliceStart; i < sliceEnd && tokensBetween.length < count; i++) {
1118+
const token = nodeTokens[i];
1119+
if (filter(token)) {
1120+
tokensBetween.push(token);
1121+
}
1122+
}
1123+
}
1124+
}
1125+
1126+
return tokensBetween;
10411127
}
1042-
/* oxlint-enable no-unused-vars */
10431128

10441129
/**
10451130
* Get the first token between two non-overlapping nodes.

apps/oxlint/test/tokens.test.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '../src-js/plugins/tokens.js';
2121
import { resetSourceAndAst } from '../src-js/plugins/source_code.js';
2222
import type { Node } from '../src-js/plugins/types.js';
23+
import type { BinaryExpression } from '../src-js/generated/types.js';
2324

2425
// Source text used for most tests
2526
const SOURCE_TEXT = '/*A*/var answer/*B*/=/*C*/a/*D*/* b/*E*///F\n call();\n/*Z*/';
@@ -50,8 +51,13 @@ beforeEach(() => {
5051
// https://eslint.org/blog/2025/10/whats-coming-in-eslint-10.0.0/#updates-to-program-ast-node-range-coverage
5152
// https://github.com/typescript-eslint/typescript-eslint/issues/11026#issuecomment-3421887632
5253
const Program = { range: [5, 55] } as Node;
53-
const BinaryExpression = { range: [26, 35] } as Node;
54+
const BinaryExpression = {
55+
range: [26, 35],
56+
left: { range: [26, 27] } as Node,
57+
right: { range: [34, 35] } as Node,
58+
} as BinaryExpression;
5459
const VariableDeclaratorIdentifier = { range: [9, 15] } as Node;
60+
const CallExpression = { range: [48, 54] } as Node;
5561

5662
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L62
5763
describe('when calling getTokens', () => {
@@ -748,11 +754,29 @@ describe('when calling getLastTokenBetween', () => {
748754
getLastTokenBetween;
749755
});
750756

757+
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L1489-L1524
751758
describe('when calling getTokensBetween', () => {
752-
/* oxlint-disable-next-line no-disabled-tests expect-expect */
753-
it('is to be implemented');
754-
/* oxlint-disable-next-line no-unused-expressions */
755-
getTokensBetween;
759+
it('should retrieve zero tokens between adjacent nodes', () => {
760+
expect(getTokensBetween(BinaryExpression, CallExpression).map((token) => token.value)).toEqual([]);
761+
});
762+
763+
it('should retrieve one token between nodes', () => {
764+
expect(getTokensBetween(BinaryExpression.left, BinaryExpression.right).map((token) => token.value)).toEqual(['*']);
765+
});
766+
767+
it('should retrieve multiple tokens between non-adjacent nodes', () => {
768+
expect(getTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right).map((token) => token.value)).toEqual([
769+
'=',
770+
'a',
771+
'*',
772+
]);
773+
});
774+
775+
it('should retrieve surrounding tokens when asked for padding', () => {
776+
expect(
777+
getTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.left, 2).map((token) => token.value),
778+
).toEqual(['var', 'answer', '=', 'a', '*']);
779+
});
756780
});
757781

758782
describe('when calling getTokenByRangeStart', () => {

0 commit comments

Comments
 (0)