Skip to content

Commit 9a548dd

Browse files
authored
feat(linter/plugins): implement SourceCode#getLastTokens() (#16000)
- Part of #14829 (comment). - Follow up to #15861.
1 parent 0b6cb11 commit 9a548dd

File tree

2 files changed

+224
-4
lines changed

2 files changed

+224
-4
lines changed

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

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,104 @@ export function getLastToken(node: Node, skipOptions?: SkipOptions | number | Fi
397397
* @param countOptions? - Options object. Same options as `getFirstTokens()`.
398398
* @returns Array of `Token`s.
399399
*/
400-
// oxlint-disable-next-line no-unused-vars
401400
export function getLastTokens(node: Node, countOptions?: CountOptions | number | FilterFn | null): Token[] {
402-
throw new Error('`sourceCode.getLastTokens` not implemented yet'); // TODO
401+
if (tokens === null) initTokens();
402+
debugAssertIsNonNull(tokens);
403+
debugAssertIsNonNull(comments);
404+
405+
// Maximum number of tokens to return
406+
const count =
407+
typeof countOptions === 'number'
408+
? countOptions
409+
: typeof countOptions === 'object' && countOptions !== null
410+
? countOptions.count
411+
: null;
412+
413+
// Function to filter tokens
414+
const filter =
415+
typeof countOptions === 'function'
416+
? countOptions
417+
: typeof countOptions === 'object' && countOptions !== null
418+
? countOptions.filter
419+
: null;
420+
421+
// Whether to return comment tokens
422+
const includeComments =
423+
typeof countOptions === 'object' &&
424+
countOptions !== null &&
425+
'includeComments' in countOptions &&
426+
countOptions.includeComments;
427+
428+
// Source array of tokens to search in
429+
let nodeTokens: Token[] | null = null;
430+
if (includeComments) {
431+
if (tokensWithComments === null) {
432+
tokensWithComments = [...tokens, ...comments].sort((a, b) => a.range[0] - b.range[0]);
433+
}
434+
nodeTokens = tokensWithComments;
435+
} else {
436+
nodeTokens = tokens;
437+
}
438+
439+
const { range } = node,
440+
rangeStart = range[0],
441+
rangeEnd = range[1];
442+
443+
// Binary search for first token within `node`'s range
444+
const tokensLength = nodeTokens.length;
445+
let sliceStart = tokensLength;
446+
for (let lo = 0; lo < sliceStart; ) {
447+
const mid = (lo + sliceStart) >> 1;
448+
if (nodeTokens[mid].range[0] < rangeStart) {
449+
lo = mid + 1;
450+
} else {
451+
sliceStart = mid;
452+
}
453+
}
454+
455+
// Binary search for the first token outside `node`'s range
456+
let sliceEnd = tokensLength;
457+
for (let lo = sliceStart; lo < sliceEnd; ) {
458+
const mid = (lo + sliceEnd) >> 1;
459+
if (nodeTokens[mid].range[0] < rangeEnd) {
460+
lo = mid + 1;
461+
} else {
462+
sliceEnd = mid;
463+
}
464+
}
465+
466+
let lastTokens: Token[] = [];
467+
if (typeof filter !== 'function') {
468+
if (typeof count !== 'number') {
469+
lastTokens = nodeTokens.slice(sliceStart, sliceEnd);
470+
} else {
471+
lastTokens = nodeTokens.slice(max(sliceStart, sliceEnd - count), sliceEnd);
472+
}
473+
} else {
474+
if (typeof count !== 'number') {
475+
lastTokens = [];
476+
for (let i = sliceStart; i < sliceEnd; i++) {
477+
const token = nodeTokens[i];
478+
if (filter(token)) {
479+
lastTokens.push(token);
480+
}
481+
}
482+
} else {
483+
lastTokens = [];
484+
// Count is the number of tokens within range from the end so we iterate in reverse
485+
for (let i = sliceEnd - 1; i >= sliceStart; i--) {
486+
const token = nodeTokens[i];
487+
if (filter(token)) {
488+
lastTokens.unshift(token);
489+
if (lastTokens.length === count) {
490+
break;
491+
}
492+
}
493+
}
494+
}
495+
}
496+
497+
return lastTokens;
403498
}
404499

405500
/**

apps/oxlint/test/tokens.test.ts

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ import {
77
getTokensAfter,
88
getTokenAfter,
99
getFirstTokens,
10+
getFirstToken,
11+
getLastTokens,
12+
getLastToken,
13+
getFirstTokensBetween,
14+
getFirstTokenBetween,
15+
getLastTokenBetween,
16+
getLastTokensBetween,
17+
getTokenByRangeStart,
18+
getTokensBetween,
19+
getTokenOrCommentBefore,
20+
getTokenOrCommentAfter,
1021
} from '../src-js/plugins/tokens.js';
1122
import { resetSourceAndAst } from '../src-js/plugins/source_code.js';
1223
import type { Node } from '../src-js/plugins/types.js';
@@ -585,64 +596,178 @@ describe('when calling getFirstTokens', () => {
585596
describe('when calling getFirstToken', () => {
586597
/* oxlint-disable-next-line no-disabled-tests expect-expect */
587598
it('is to be implemented');
599+
/* oxlint-disable-next-line no-unused-expressions */
600+
getFirstToken;
588601
});
589602

603+
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L851-L930
590604
describe('when calling getLastTokens', () => {
591-
/* oxlint-disable-next-line no-disabled-tests expect-expect */
592-
it('is to be implemented');
605+
it("should retrieve zero tokens from the end of a node's token stream", () => {
606+
assert.deepStrictEqual(
607+
getLastTokens(BinaryExpression, 0).map((token) => token.value),
608+
[],
609+
);
610+
});
611+
612+
it("should retrieve one token from the end of a node's token stream", () => {
613+
assert.deepStrictEqual(
614+
getLastTokens(BinaryExpression, 1).map((token) => token.value),
615+
['b'],
616+
);
617+
});
618+
619+
it("should retrieve more than one token from the end of a node's token stream", () => {
620+
assert.deepStrictEqual(
621+
getLastTokens(BinaryExpression, 2).map((token) => token.value),
622+
['*', 'b'],
623+
);
624+
});
625+
626+
it("should retrieve all tokens from the end of a node's token stream", () => {
627+
assert.deepStrictEqual(
628+
getLastTokens(BinaryExpression, 9e9).map((token) => token.value),
629+
['a', '*', 'b'],
630+
);
631+
});
632+
633+
it("should retrieve more than one token from the end of a node's token stream with count option", () => {
634+
assert.deepStrictEqual(
635+
getLastTokens(BinaryExpression, { count: 2 }).map((token) => token.value),
636+
['*', 'b'],
637+
);
638+
});
639+
640+
it("should retrieve matched tokens from the end of a node's token stream with filter option", () => {
641+
assert.deepStrictEqual(
642+
getLastTokens(BinaryExpression, (t) => t.type === 'Identifier').map((token) => token.value),
643+
['a', 'b'],
644+
);
645+
assert.deepStrictEqual(
646+
getLastTokens(BinaryExpression, {
647+
filter: (t) => t.type === 'Identifier',
648+
}).map((token) => token.value),
649+
['a', 'b'],
650+
);
651+
});
652+
653+
it("should retrieve matched tokens from the end of a node's token stream with filter and count options", () => {
654+
assert.deepStrictEqual(
655+
getLastTokens(BinaryExpression, {
656+
count: 1,
657+
filter: (t) => t.type === 'Identifier',
658+
}).map((token) => token.value),
659+
['b'],
660+
);
661+
});
662+
663+
it("should retrieve all tokens from the end of a node's token stream with includeComments option", () => {
664+
assert.deepStrictEqual(
665+
getLastTokens(BinaryExpression, {
666+
includeComments: true,
667+
}).map((token) => token.value),
668+
['a', 'D', '*', 'b'],
669+
);
670+
});
671+
672+
it("should retrieve matched tokens from the end of a node's token stream with includeComments and count options", () => {
673+
assert.deepStrictEqual(
674+
getLastTokens(BinaryExpression, {
675+
includeComments: true,
676+
count: 3,
677+
}).map((token) => token.value),
678+
['D', '*', 'b'],
679+
);
680+
});
681+
682+
it("should retrieve matched tokens from the end of a node's token stream with includeComments and count and filter options", () => {
683+
assert.deepStrictEqual(
684+
getLastTokens(BinaryExpression, {
685+
includeComments: true,
686+
count: 3,
687+
filter: (t) => t.type !== 'Punctuator',
688+
}).map((token) => token.value),
689+
['a', 'D', 'b'],
690+
);
691+
});
593692
});
594693

595694
describe('when calling getLastToken', () => {
596695
/* oxlint-disable-next-line no-disabled-tests expect-expect */
597696
it('is to be implemented');
697+
/* oxlint-disable-next-line no-unused-expressions */
698+
getLastToken;
598699
});
599700

600701
describe('when calling getFirstTokensBetween', () => {
601702
/* oxlint-disable-next-line no-disabled-tests expect-expect */
602703
it('is to be implemented');
704+
/* oxlint-disable-next-line no-unused-expressions */
705+
getFirstTokensBetween;
603706
});
604707

605708
describe('when calling getFirstTokenBetween', () => {
606709
/* oxlint-disable-next-line no-disabled-tests expect-expect */
607710
it('is to be implemented');
711+
/* oxlint-disable-next-line no-unused-expressions */
712+
getFirstTokenBetween;
608713
});
609714

610715
describe('when calling getLastTokensBetween', () => {
611716
/* oxlint-disable-next-line no-disabled-tests expect-expect */
612717
it('is to be implemented');
718+
/* oxlint-disable-next-line no-unused-expressions */
719+
getLastTokensBetween;
613720
});
614721

615722
describe('when calling getLastTokenBetween', () => {
616723
/* oxlint-disable-next-line no-disabled-tests expect-expect */
617724
it('is to be implemented');
725+
/* oxlint-disable-next-line no-unused-expressions */
726+
getLastTokenBetween;
618727
});
619728

620729
describe('when calling getTokensBetween', () => {
621730
/* oxlint-disable-next-line no-disabled-tests expect-expect */
622731
it('is to be implemented');
732+
/* oxlint-disable-next-line no-unused-expressions */
733+
getTokensBetween;
623734
});
624735

625736
describe('when calling getTokenByRangeStart', () => {
626737
/* oxlint-disable-next-line no-disabled-tests expect-expect */
627738
it('is to be implemented');
739+
/* oxlint-disable-next-line no-unused-expressions */
740+
getTokenByRangeStart;
628741
});
629742

630743
describe('when calling getTokenOrCommentBefore', () => {
631744
/* oxlint-disable-next-line no-disabled-tests expect-expect */
632745
it('is to be implemented');
746+
/* oxlint-disable-next-line no-unused-expressions */
747+
getTokenOrCommentBefore;
633748
});
634749

635750
describe('when calling getTokenOrCommentAfter', () => {
636751
/* oxlint-disable-next-line no-disabled-tests expect-expect */
637752
it('is to be implemented');
753+
/* oxlint-disable-next-line no-unused-expressions */
754+
getTokenOrCommentAfter;
638755
});
639756

640757
describe('when calling getFirstToken & getTokenAfter', () => {
641758
/* oxlint-disable-next-line no-disabled-tests expect-expect */
642759
it('is to be implemented');
760+
/* oxlint-disable-next-line no-unused-expressions */
761+
getFirstToken;
762+
/* oxlint-disable-next-line no-unused-expressions */
763+
getTokenAfter;
643764
});
644765

645766
describe('when calling getLastToken & getTokenBefore', () => {
646767
/* oxlint-disable-next-line no-disabled-tests expect-expect */
647768
it('is to be implemented');
769+
/* oxlint-disable-next-line no-unused-expressions */
770+
getLastToken;
771+
/* oxlint-disable-next-line no-unused-expressions */
772+
getTokenBefore;
648773
});

0 commit comments

Comments
 (0)