Skip to content

Commit 1f262a9

Browse files
lilnasyoverlookmotel
authored andcommitted
feat(linter/plugins): implement SourceCode#getFirstTokensBetween()
1 parent 78f74b1 commit 1f262a9

File tree

2 files changed

+151
-12
lines changed

2 files changed

+151
-12
lines changed

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

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,22 +1162,110 @@ export function getFirstTokenBetween(
11621162

11631163
/**
11641164
* Get the first tokens between two non-overlapping nodes.
1165-
* @param nodeOrToken1 - Node before the desired token range.
1166-
* @param nodeOrToken2 - Node after the desired token range.
1165+
* @param left - Node or token before the desired token range.
1166+
* @param right - Node or token after the desired token range.
11671167
* @param countOptions? - Options object.
11681168
* If is a number, equivalent to `{ count: n }`.
11691169
* If is a function, equivalent to `{ filter: fn }`.
1170-
* @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`.
1170+
* @returns Array of `Token`s between `left` and `right`.
11711171
*/
1172-
/* oxlint-disable no-unused-vars */
11731172
export function getFirstTokensBetween(
1174-
nodeOrToken1: NodeOrToken | Comment,
1175-
nodeOrToken2: NodeOrToken | Comment,
1173+
left: NodeOrToken | Comment,
1174+
right: NodeOrToken | Comment,
11761175
countOptions?: CountOptions | number | FilterFn | null,
11771176
): Token[] {
1178-
throw new Error('`sourceCode.getFirstTokensBetween` not implemented yet'); // TODO
1177+
if (tokens === null) initTokens();
1178+
debugAssertIsNonNull(tokens);
1179+
debugAssertIsNonNull(comments);
1180+
1181+
const count =
1182+
typeof countOptions === 'number'
1183+
? countOptions
1184+
: typeof countOptions === 'object' && countOptions !== null
1185+
? countOptions.count
1186+
: null;
1187+
1188+
const filter =
1189+
typeof countOptions === 'function'
1190+
? countOptions
1191+
: typeof countOptions === 'object' && countOptions !== null
1192+
? countOptions.filter
1193+
: null;
1194+
1195+
const includeComments =
1196+
typeof countOptions === 'object' &&
1197+
countOptions !== null &&
1198+
'includeComments' in countOptions &&
1199+
countOptions.includeComments;
1200+
1201+
let nodeTokens: Token[] | null = null;
1202+
if (includeComments) {
1203+
if (tokensWithComments === null) initTokensWithComments();
1204+
debugAssertIsNonNull(tokensWithComments);
1205+
nodeTokens = tokensWithComments;
1206+
} else {
1207+
nodeTokens = tokens;
1208+
}
1209+
1210+
// This range is not invariant over node order.
1211+
// The first argument must be the left node.
1212+
// Same as ESLint's implementation.
1213+
const rangeStart = left.range[1],
1214+
rangeEnd = right.range[0];
1215+
1216+
const tokensLength = nodeTokens.length;
1217+
1218+
// Find the first token after `left`
1219+
let sliceStart = tokensLength;
1220+
for (let lo = 0; lo < sliceStart; ) {
1221+
const mid = (lo + sliceStart) >> 1;
1222+
if (nodeTokens[mid].range[0] < rangeStart) {
1223+
lo = mid + 1;
1224+
} else {
1225+
sliceStart = mid;
1226+
}
1227+
}
1228+
1229+
// Find the first token at or after `right`
1230+
let sliceEnd = tokensLength;
1231+
for (let lo = sliceStart; lo < sliceEnd; ) {
1232+
const mid = (lo + sliceEnd) >> 1;
1233+
if (nodeTokens[mid].range[0] < rangeEnd) {
1234+
lo = mid + 1;
1235+
} else {
1236+
sliceEnd = mid;
1237+
}
1238+
}
1239+
1240+
let firstTokens: Token[];
1241+
if (typeof filter !== 'function') {
1242+
if (typeof count !== 'number') {
1243+
firstTokens = nodeTokens.slice(sliceStart, sliceEnd);
1244+
} else {
1245+
firstTokens = nodeTokens.slice(sliceStart, min(sliceStart + count, sliceEnd));
1246+
}
1247+
} else {
1248+
if (typeof count !== 'number') {
1249+
firstTokens = [];
1250+
for (let i = sliceStart; i < sliceEnd; i++) {
1251+
const token = nodeTokens[i];
1252+
if (filter(token)) {
1253+
firstTokens.push(token);
1254+
}
1255+
}
1256+
} else {
1257+
firstTokens = [];
1258+
for (let i = sliceStart; i < sliceEnd && firstTokens.length < count; i++) {
1259+
const token = nodeTokens[i];
1260+
if (filter(token)) {
1261+
firstTokens.push(token);
1262+
}
1263+
}
1264+
}
1265+
}
1266+
1267+
return firstTokens;
11791268
}
1180-
/* oxlint-enable no-unused-vars */
11811269

11821270
/**
11831271
* Get the last token between two non-overlapping nodes.

apps/oxlint/test/tokens.test.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -900,11 +900,62 @@ describe('when calling getLastTokenBetween', () => {
900900
});
901901
});
902902

903+
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L1107-L1191
903904
describe('when calling getFirstTokensBetween', () => {
904-
/* oxlint-disable-next-line no-disabled-tests expect-expect */
905-
it('is to be implemented');
906-
/* oxlint-disable-next-line no-unused-expressions */
907-
getFirstTokensBetween;
905+
it('should retrieve zero tokens between adjacent nodes', () => {
906+
expect(getFirstTokensBetween(BinaryExpression, CallExpression).map((token) => token.value)).toEqual([]);
907+
});
908+
909+
it('should retrieve multiple tokens between non-adjacent nodes with count option', () => {
910+
expect(
911+
getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, 2).map((token) => token.value),
912+
).toEqual(['=', 'a']);
913+
expect(
914+
getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { count: 2 }).map(
915+
(token) => token.value,
916+
),
917+
).toEqual(['=', 'a']);
918+
});
919+
920+
it('should retrieve matched tokens between non-adjacent nodes with filter option', () => {
921+
expect(
922+
getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
923+
filter: (t) => t.type !== 'Punctuator',
924+
}).map((token) => token.value),
925+
).toEqual(['a']);
926+
});
927+
928+
it('should retrieve all tokens between non-adjacent nodes with empty object option', () => {
929+
expect(
930+
getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {}).map((token) => token.value),
931+
).toEqual(['=', 'a', '*']);
932+
});
933+
934+
it('should retrieve multiple tokens between non-adjacent nodes with includeComments option', () => {
935+
expect(
936+
getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { includeComments: true }).map(
937+
(token) => token.value,
938+
),
939+
).toEqual(['B', '=', 'C', 'a', 'D', '*']);
940+
});
941+
942+
it('should retrieve multiple tokens between non-adjacent nodes with includeComments and count options', () => {
943+
expect(
944+
getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
945+
includeComments: true,
946+
count: 3,
947+
}).map((token) => token.value),
948+
).toEqual(['B', '=', 'C']);
949+
});
950+
951+
it('should retrieve multiple tokens and comments between non-adjacent nodes with includeComments and filter options', () => {
952+
expect(
953+
getFirstTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
954+
includeComments: true,
955+
filter: (t) => t.type !== 'Punctuator',
956+
}).map((token) => token.value),
957+
).toEqual(['B', 'C', 'a', 'D']);
958+
});
908959
});
909960

910961
describe('when calling getFirstTokenBetween', () => {

0 commit comments

Comments
 (0)