Skip to content

Commit 79c242f

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

File tree

2 files changed

+152
-12
lines changed

2 files changed

+152
-12
lines changed

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

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,22 +1439,111 @@ export function getLastTokenBetween(
14391439

14401440
/**
14411441
* Get the last tokens between two non-overlapping nodes.
1442-
* @param nodeOrToken1 - Node before the desired token range.
1443-
* @param nodeOrToken2 - Node after the desired token range.
1442+
* @param left - Node or token before the desired token range.
1443+
* @param right - Node or token after the desired token range.
14441444
* @param countOptions? - Options object.
14451445
* If is a number, equivalent to `{ count: n }`.
14461446
* If is a function, equivalent to `{ filter: fn }`.
1447-
* @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`.
1447+
* @returns Array of `Token`s between `left` and `right`.
14481448
*/
1449-
/* oxlint-disable no-unused-vars */
14501449
export function getLastTokensBetween(
1451-
nodeOrToken1: NodeOrToken | Comment,
1452-
nodeOrToken2: NodeOrToken | Comment,
1450+
left: NodeOrToken | Comment,
1451+
right: NodeOrToken | Comment,
14531452
countOptions?: CountOptions | number | FilterFn | null,
14541453
): Token[] {
1455-
throw new Error('`sourceCode.getLastTokensBetween` not implemented yet'); // TODO
1454+
if (tokens === null) initTokens();
1455+
debugAssertIsNonNull(tokens);
1456+
debugAssertIsNonNull(comments);
1457+
1458+
const count =
1459+
typeof countOptions === 'number'
1460+
? countOptions
1461+
: typeof countOptions === 'object' && countOptions !== null
1462+
? countOptions.count
1463+
: null;
1464+
1465+
const filter =
1466+
typeof countOptions === 'function'
1467+
? countOptions
1468+
: typeof countOptions === 'object' && countOptions !== null
1469+
? countOptions.filter
1470+
: null;
1471+
1472+
const includeComments =
1473+
typeof countOptions === 'object' &&
1474+
countOptions !== null &&
1475+
'includeComments' in countOptions &&
1476+
countOptions.includeComments;
1477+
1478+
let nodeTokens: Token[] | null = null;
1479+
if (includeComments) {
1480+
if (tokensWithComments === null) initTokensWithComments();
1481+
debugAssertIsNonNull(tokensWithComments);
1482+
nodeTokens = tokensWithComments;
1483+
} else {
1484+
nodeTokens = tokens;
1485+
}
1486+
1487+
// This range is not invariant over node order.
1488+
// The first argument must be the left node.
1489+
// Same as ESLint's implementation.
1490+
const rangeStart = left.range[1],
1491+
rangeEnd = right.range[0],
1492+
tokensLength = nodeTokens.length;
1493+
1494+
// Binary search for first token past the beginning of the `between` range
1495+
let sliceStart = tokensLength;
1496+
for (let lo = 0; lo < sliceStart; ) {
1497+
const mid = (lo + sliceStart) >> 1;
1498+
if (nodeTokens[mid].range[0] < rangeStart) {
1499+
lo = mid + 1;
1500+
} else {
1501+
sliceStart = mid;
1502+
}
1503+
}
1504+
1505+
// Binary search for first token past the end of the `between` range
1506+
let sliceEnd = tokensLength;
1507+
for (let lo = sliceStart; lo < sliceEnd; ) {
1508+
const mid = (lo + sliceEnd) >> 1;
1509+
if (nodeTokens[mid].range[0] < rangeEnd) {
1510+
lo = mid + 1;
1511+
} else {
1512+
sliceEnd = mid;
1513+
}
1514+
}
1515+
1516+
let tokensBetween: Token[];
1517+
// Fast path for the common case
1518+
if (typeof filter !== 'function') {
1519+
if (typeof count !== 'number') {
1520+
tokensBetween = nodeTokens.slice(sliceStart, sliceEnd);
1521+
} else {
1522+
tokensBetween = nodeTokens.slice(max(sliceStart, sliceEnd - count), sliceEnd);
1523+
}
1524+
} else {
1525+
if (typeof count !== 'number') {
1526+
tokensBetween = [];
1527+
for (let i = sliceStart; i < sliceEnd; i++) {
1528+
const token = nodeTokens[i];
1529+
if (filter(token)) {
1530+
tokensBetween.push(token);
1531+
}
1532+
}
1533+
} else {
1534+
tokensBetween = [];
1535+
// Count is the number of preceding tokens so we iterate in reverse
1536+
for (let i = sliceEnd - 1; i >= sliceStart && tokensBetween.length < count; i--) {
1537+
const token = nodeTokens[i];
1538+
if (filter(token)) {
1539+
tokensBetween.unshift(token);
1540+
}
1541+
}
1542+
}
1543+
}
1544+
1545+
return tokensBetween;
14561546
}
1457-
/* oxlint-enable no-unused-vars */
14581547

14591548
/**
14601549
* Get the token starting at the specified index.

apps/oxlint/test/tokens.test.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,11 +1046,62 @@ describe('when calling getFirstTokenBetween', () => {
10461046
});
10471047
});
10481048

1049+
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L1298-L1382
10491050
describe('when calling getLastTokensBetween', () => {
1050-
/* oxlint-disable-next-line no-disabled-tests expect-expect */
1051-
it('is to be implemented');
1052-
/* oxlint-disable-next-line no-unused-expressions */
1053-
getLastTokensBetween;
1051+
it('should retrieve zero tokens between adjacent nodes', () => {
1052+
expect(getLastTokensBetween(BinaryExpression, CallExpression).map((token) => token.value)).toEqual([]);
1053+
});
1054+
1055+
it('should retrieve multiple tokens between non-adjacent nodes with count option', () => {
1056+
expect(
1057+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, 2).map((token) => token.value),
1058+
).toEqual(['a', '*']);
1059+
expect(
1060+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { count: 2 }).map(
1061+
(token) => token.value,
1062+
),
1063+
).toEqual(['a', '*']);
1064+
});
1065+
1066+
it('should retrieve matched tokens between non-adjacent nodes with filter option', () => {
1067+
expect(
1068+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
1069+
filter: (t) => t.type !== 'Punctuator',
1070+
}).map((token) => token.value),
1071+
).toEqual(['a']);
1072+
});
1073+
1074+
it('should retrieve all tokens between non-adjacent nodes with empty object option', () => {
1075+
expect(
1076+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {}).map((token) => token.value),
1077+
).toEqual(['=', 'a', '*']);
1078+
});
1079+
1080+
it('should retrieve all tokens and comments between non-adjacent nodes with includeComments option', () => {
1081+
expect(
1082+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { includeComments: true }).map(
1083+
(token) => token.value,
1084+
),
1085+
).toEqual(['B', '=', 'C', 'a', 'D', '*']);
1086+
});
1087+
1088+
it('should retrieve multiple tokens between non-adjacent nodes with includeComments and count options', () => {
1089+
expect(
1090+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
1091+
includeComments: true,
1092+
count: 3,
1093+
}).map((token) => token.value),
1094+
).toEqual(['a', 'D', '*']);
1095+
});
1096+
1097+
it('should retrieve multiple tokens and comments between non-adjacent nodes with includeComments and filter options', () => {
1098+
expect(
1099+
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
1100+
includeComments: true,
1101+
filter: (t) => t.type !== 'Punctuator',
1102+
}).map((token) => token.value),
1103+
).toEqual(['B', 'C', 'a', 'D']);
1104+
});
10541105
});
10551106

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

0 commit comments

Comments
 (0)