Skip to content

Commit 7b8d578

Browse files
authored
feat(linter/plugins): implement SourceCode#getTokensBetween() (#16034)
- Part of #14829 (comment). - Follow up to #15861.
1 parent 79c242f commit 7b8d578

File tree

2 files changed

+121
-15
lines changed

2 files changed

+121
-15
lines changed

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

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,27 +1105,115 @@ export function getTokensAfter(
11051105

11061106
/**
11071107
* Get all of the tokens between two non-overlapping nodes.
1108-
* @param nodeOrToken1 - Node before the desired token range.
1109-
* @param nodeOrToken2 - Node after the desired token range.
1108+
* @param left - Node or token before the desired token range.
1109+
* @param right - Node or token after the desired token range.
11101110
* @param countOptions? - Options object. If this is a function then it's `options.filter`.
1111-
* @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`.
1111+
* @returns Array of `Token`s between `left` and `right`.
11121112
*/
11131113
/**
11141114
* Get all of the tokens between two non-overlapping nodes.
1115-
* @param nodeOrToken1 - Node before the desired token range.
1116-
* @param nodeOrToken2 - Node after the desired token range.
1115+
* @param left - Node or token before the desired token range.
1116+
* @param right - Node or token after the desired token range.
11171117
* @param padding - Number of extra tokens on either side of center.
1118-
* @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`.
1118+
* @returns Array of `Token`s between `left` and `right`.
11191119
*/
1120-
/* oxlint-disable no-unused-vars */
11211120
export function getTokensBetween(
1122-
nodeOrToken1: NodeOrToken | Comment,
1123-
nodeOrToken2: NodeOrToken | Comment,
1121+
left: NodeOrToken | Comment,
1122+
right: NodeOrToken | Comment,
11241123
countOptions?: CountOptions | number | FilterFn | null,
11251124
): Token[] {
1126-
throw new Error('`sourceCode.getTokensBetween` not implemented yet'); // TODO
1125+
if (tokens === null) initTokens();
1126+
debugAssertIsNonNull(tokens);
1127+
debugAssertIsNonNull(comments);
1128+
1129+
const count = typeof countOptions === 'object' && countOptions !== null ? countOptions.count : null;
1130+
1131+
const padding = typeof countOptions === 'number' ? countOptions : 0;
1132+
1133+
const filter =
1134+
typeof countOptions === 'function'
1135+
? countOptions
1136+
: typeof countOptions === 'object' && countOptions !== null
1137+
? countOptions.filter
1138+
: null;
1139+
1140+
const includeComments =
1141+
typeof countOptions === 'object' &&
1142+
countOptions !== null &&
1143+
'includeComments' in countOptions &&
1144+
countOptions.includeComments;
1145+
1146+
let nodeTokens: Token[];
1147+
if (includeComments) {
1148+
if (tokensWithComments === null) initTokensWithComments();
1149+
debugAssertIsNonNull(tokensWithComments);
1150+
nodeTokens = tokensWithComments;
1151+
} else {
1152+
nodeTokens = tokens;
1153+
}
1154+
1155+
// This range is not invariant over node order.
1156+
// The first argument must be the left node.
1157+
// Same as ESLint's implementation.
1158+
const rangeStart = left.range[1],
1159+
rangeEnd = right.range[0],
1160+
tokensLength = nodeTokens.length;
1161+
1162+
// Binary search for first token past the beginning of the `between` range
1163+
let sliceStart = tokensLength;
1164+
for (let lo = 0; lo < sliceStart; ) {
1165+
const mid = (lo + sliceStart) >> 1;
1166+
if (nodeTokens[mid].range[0] < rangeStart) {
1167+
lo = mid + 1;
1168+
} else {
1169+
sliceStart = mid;
1170+
}
1171+
}
1172+
1173+
// Binary search for first token past the end of the `between` range
1174+
let sliceEnd = tokensLength;
1175+
for (let lo = sliceStart; lo < sliceEnd; ) {
1176+
const mid = (lo + sliceEnd) >> 1;
1177+
if (nodeTokens[mid].range[0] < rangeEnd) {
1178+
lo = mid + 1;
1179+
} else {
1180+
sliceEnd = mid;
1181+
}
1182+
}
1183+
1184+
// Apply padding
1185+
sliceStart = max(0, sliceStart - padding);
1186+
sliceEnd += padding;
1187+
1188+
let tokensBetween: Token[];
1189+
if (typeof filter !== 'function') {
1190+
if (typeof count !== 'number') {
1191+
tokensBetween = nodeTokens.slice(sliceStart, sliceEnd);
1192+
} else {
1193+
tokensBetween = nodeTokens.slice(sliceStart, min(sliceStart + count, sliceEnd));
1194+
}
1195+
} else {
1196+
if (typeof count !== 'number') {
1197+
tokensBetween = [];
1198+
for (let i = sliceStart; i < sliceEnd; i++) {
1199+
const token = nodeTokens[i];
1200+
if (filter(token)) {
1201+
tokensBetween.push(token);
1202+
}
1203+
}
1204+
} else {
1205+
tokensBetween = [];
1206+
for (let i = sliceStart; i < sliceEnd && tokensBetween.length < count; i++) {
1207+
const token = nodeTokens[i];
1208+
if (filter(token)) {
1209+
tokensBetween.push(token);
1210+
}
1211+
}
1212+
}
1213+
}
1214+
1215+
return tokensBetween;
11271216
}
1128-
/* oxlint-enable no-unused-vars */
11291217

11301218
/**
11311219
* Get the first token between two non-overlapping nodes.

apps/oxlint/test/tokens.test.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,11 +1111,29 @@ describe('when calling getLastTokenBetween', () => {
11111111
getLastTokenBetween;
11121112
});
11131113

1114+
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L1489-L1524
11141115
describe('when calling getTokensBetween', () => {
1115-
/* oxlint-disable-next-line no-disabled-tests expect-expect */
1116-
it('is to be implemented');
1117-
/* oxlint-disable-next-line no-unused-expressions */
1118-
getTokensBetween;
1116+
it('should retrieve zero tokens between adjacent nodes', () => {
1117+
expect(getTokensBetween(BinaryExpression, CallExpression).map((token) => token.value)).toEqual([]);
1118+
});
1119+
1120+
it('should retrieve one token between nodes', () => {
1121+
expect(getTokensBetween(BinaryExpression.left, BinaryExpression.right).map((token) => token.value)).toEqual(['*']);
1122+
});
1123+
1124+
it('should retrieve multiple tokens between non-adjacent nodes', () => {
1125+
expect(getTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right).map((token) => token.value)).toEqual([
1126+
'=',
1127+
'a',
1128+
'*',
1129+
]);
1130+
});
1131+
1132+
it('should retrieve surrounding tokens when asked for padding', () => {
1133+
expect(
1134+
getTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.left, 2).map((token) => token.value),
1135+
).toEqual(['var', 'answer', '=', 'a', '*']);
1136+
});
11191137
});
11201138

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

0 commit comments

Comments
 (0)