Skip to content

Commit 42b9c17

Browse files
committed
feat(linter/plugins): implement SourceCode#getLastTokensBetween()
1 parent bf9f469 commit 42b9c17

File tree

2 files changed

+156
-13
lines changed

2 files changed

+156
-13
lines changed

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

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,20 +1094,106 @@ export function getLastTokenBetween(
10941094

10951095
/**
10961096
* Get the last tokens between two non-overlapping nodes.
1097-
* @param nodeOrToken1 - Node before the desired token range.
1098-
* @param nodeOrToken2 - Node after the desired token range.
1097+
* @param left - Node before the desired token range.
1098+
* @param right - Node after the desired token range.
10991099
* @param countOptions? - Options object. Same options as `getFirstTokens()`.
1100-
* @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`.
1100+
* @returns Array of `Token`s between `left` and `right`.
11011101
*/
1102-
/* oxlint-disable no-unused-vars */
11031102
export function getLastTokensBetween(
1104-
nodeOrToken1: NodeOrToken | Comment,
1105-
nodeOrToken2: NodeOrToken | Comment,
1103+
left: NodeOrToken | Comment,
1104+
right: NodeOrToken | Comment,
11061105
countOptions?: CountOptions | number | FilterFn | null,
11071106
): Token[] {
1108-
throw new Error('`sourceCode.getLastTokensBetween` not implemented yet'); // TODO
1107+
if (tokens === null) initTokens();
1108+
debugAssertIsNonNull(tokens);
1109+
debugAssertIsNonNull(comments);
1110+
1111+
const count =
1112+
typeof countOptions === 'number'
1113+
? countOptions
1114+
: typeof countOptions === 'object' && countOptions !== null
1115+
? countOptions.count
1116+
: null;
1117+
1118+
const filter =
1119+
typeof countOptions === 'function'
1120+
? countOptions
1121+
: typeof countOptions === 'object' && countOptions !== null
1122+
? countOptions.filter
1123+
: null;
1124+
1125+
const includeComments =
1126+
typeof countOptions === 'object' &&
1127+
countOptions !== null &&
1128+
'includeComments' in countOptions &&
1129+
countOptions.includeComments;
1130+
1131+
let nodeTokens: Token[] | null = null;
1132+
if (includeComments) {
1133+
if (tokensWithComments === null) initTokensWithComments();
1134+
debugAssertIsNonNull(tokensWithComments);
1135+
nodeTokens = tokensWithComments;
1136+
} else {
1137+
nodeTokens = tokens;
1138+
}
1139+
1140+
const rangeStart = left.range[1],
1141+
rangeEnd = right.range[0],
1142+
tokensLength = nodeTokens.length;
1143+
1144+
// Binary search for first token past the beginning of the `between` range
1145+
let sliceStart = tokensLength;
1146+
for (let lo = 0; lo < sliceStart; ) {
1147+
const mid = (lo + sliceStart) >> 1;
1148+
if (nodeTokens[mid].range[0] < rangeStart) {
1149+
lo = mid + 1;
1150+
} else {
1151+
sliceStart = mid;
1152+
}
1153+
}
1154+
1155+
// Binary search for first token past the end of the `between` range
1156+
let sliceEnd = tokensLength;
1157+
for (let lo = sliceStart; lo < sliceEnd; ) {
1158+
const mid = (lo + sliceEnd) >> 1;
1159+
if (nodeTokens[mid].range[0] < rangeEnd) {
1160+
lo = mid + 1;
1161+
} else {
1162+
sliceEnd = mid;
1163+
}
1164+
}
1165+
1166+
let tokensBetween: Token[];
1167+
// Fast path for the common case
1168+
if (typeof filter !== 'function') {
1169+
if (typeof count !== 'number') {
1170+
tokensBetween = nodeTokens.slice(sliceStart, sliceEnd);
1171+
} else {
1172+
tokensBetween = nodeTokens.slice(max(sliceStart, sliceEnd - count), sliceEnd);
1173+
}
1174+
} else {
1175+
if (typeof count !== 'number') {
1176+
tokensBetween = [];
1177+
for (let i = sliceStart; i < sliceEnd; i++) {
1178+
const token = nodeTokens[i];
1179+
if (filter(token)) {
1180+
tokensBetween.push(token);
1181+
}
1182+
}
1183+
} else {
1184+
tokensBetween = [];
1185+
// Count is the number of preceding tokens so we iterate in reverse
1186+
for (let i = sliceEnd - 1; i >= sliceStart && tokensBetween.length < count; i--) {
1187+
const token = nodeTokens[i];
1188+
if (filter(token)) {
1189+
tokensBetween.unshift(token);
1190+
}
1191+
}
1192+
}
1193+
}
1194+
1195+
return tokensBetween;
11091196
}
1110-
/* oxlint-enable no-unused-vars */
11111197

11121198
/**
11131199
* Get the token starting at the specified index.

apps/oxlint/test/tokens.test.ts

Lines changed: 62 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', () => {
@@ -734,11 +740,62 @@ describe('when calling getFirstTokenBetween', () => {
734740
getFirstTokenBetween;
735741
});
736742

743+
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L1298-L1382
737744
describe('when calling getLastTokensBetween', () => {
738-
/* oxlint-disable-next-line no-disabled-tests expect-expect */
739-
it('is to be implemented');
740-
/* oxlint-disable-next-line no-unused-expressions */
741-
getLastTokensBetween;
745+
it('should retrieve zero tokens between adjacent nodes', () => {
746+
expect(getLastTokensBetween(BinaryExpression, CallExpression).map((token) => token.value)).toEqual([]);
747+
});
748+
749+
it('should retrieve multiple tokens between non-adjacent nodes with count option', () => {
750+
expect(
751+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, 2).map((token) => token.value),
752+
).toEqual(['a', '*']);
753+
expect(
754+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { count: 2 }).map(
755+
(token) => token.value,
756+
),
757+
).toEqual(['a', '*']);
758+
});
759+
760+
it('should retrieve matched tokens between non-adjacent nodes with filter option', () => {
761+
expect(
762+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
763+
filter: (t) => t.type !== 'Punctuator',
764+
}).map((token) => token.value),
765+
).toEqual(['a']);
766+
});
767+
768+
it('should retrieve all tokens between non-adjacent nodes with empty object option', () => {
769+
expect(
770+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {}).map((token) => token.value),
771+
).toEqual(['=', 'a', '*']);
772+
});
773+
774+
it('should retrieve all tokens and comments between non-adjacent nodes with includeComments option', () => {
775+
expect(
776+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { includeComments: true }).map(
777+
(token) => token.value,
778+
),
779+
).toEqual(['B', '=', 'C', 'a', 'D', '*']);
780+
});
781+
782+
it('should retrieve multiple tokens between non-adjacent nodes with includeComments and count options', () => {
783+
expect(
784+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
785+
includeComments: true,
786+
count: 3,
787+
}).map((token) => token.value),
788+
).toEqual(['a', 'D', '*']);
789+
});
790+
791+
it('should retrieve multiple tokens and comments between non-adjacent nodes with includeComments and filter options', () => {
792+
expect(
793+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
794+
includeComments: true,
795+
filter: (t) => t.type !== 'Punctuator',
796+
}).map((token) => token.value),
797+
).toEqual(['B', 'C', 'a', 'D']);
798+
});
742799
});
743800

744801
describe('when calling getLastTokenBetween', () => {

0 commit comments

Comments
 (0)