Skip to content

Commit 2bfdd26

Browse files
feat(linter/plugins): implement SourceCode#getTokensAfter() (#15971)
- Part of #14829 (comment). - Follow up to #15861. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 6f6e609 commit 2bfdd26

File tree

2 files changed

+167
-7
lines changed

2 files changed

+167
-7
lines changed

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

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -560,14 +560,89 @@ export function getTokenOrCommentAfter(nodeOrToken: NodeOrToken | Comment, skip?
560560
* @param countOptions? - Options object. Same options as `getFirstTokens()`.
561561
* @returns Array of `Token`s.
562562
*/
563-
/* oxlint-disable no-unused-vars */
564563
export function getTokensAfter(
565564
nodeOrToken: NodeOrToken | Comment,
566565
countOptions?: CountOptions | number | FilterFn | null,
567566
): Token[] {
568-
throw new Error('`sourceCode.getTokensAfter` not implemented yet'); // TODO
567+
if (tokens === null) initTokens();
568+
debugAssertIsNonNull(tokens);
569+
debugAssertIsNonNull(comments);
570+
571+
const count =
572+
typeof countOptions === 'number'
573+
? countOptions
574+
: typeof countOptions === 'object' && countOptions !== null
575+
? countOptions.count
576+
: null;
577+
578+
const filter =
579+
typeof countOptions === 'function'
580+
? countOptions
581+
: typeof countOptions === 'object' && countOptions !== null
582+
? countOptions.filter
583+
: null;
584+
585+
const includeComments =
586+
typeof countOptions === 'object' &&
587+
countOptions !== null &&
588+
'includeComments' in countOptions &&
589+
countOptions.includeComments;
590+
591+
let nodeTokens: Token[];
592+
if (includeComments) {
593+
if (tokensWithComments === null) {
594+
tokensWithComments = [...tokens, ...comments].sort((a, b) => a.range[0] - b.range[0]);
595+
}
596+
nodeTokens = tokensWithComments;
597+
} else {
598+
nodeTokens = tokens;
599+
}
600+
601+
const rangeEnd = nodeOrToken.range[1];
602+
603+
let sliceStart = nodeTokens.length;
604+
for (let lo = 0; lo < sliceStart; ) {
605+
const mid = (lo + sliceStart) >> 1;
606+
if (nodeTokens[mid].range[0] < rangeEnd) {
607+
lo = mid + 1;
608+
} else {
609+
sliceStart = mid;
610+
}
611+
}
612+
613+
let nodeTokensAfter: Token[];
614+
// Fast path for the common case
615+
if (typeof filter !== 'function') {
616+
if (typeof count !== 'number') {
617+
nodeTokensAfter = nodeTokens.slice(sliceStart);
618+
} else {
619+
nodeTokensAfter = nodeTokens.slice(sliceStart, sliceStart + count);
620+
}
621+
} else {
622+
if (typeof count !== 'number') {
623+
nodeTokensAfter = [];
624+
for (let i = sliceStart; i < nodeTokens.length; i++) {
625+
const token = nodeTokens[i];
626+
if (filter(token)) {
627+
nodeTokensAfter.push(token);
628+
}
629+
}
630+
} else {
631+
nodeTokensAfter = [];
632+
for (let i = sliceStart; i < nodeTokens.length; i++) {
633+
const token = nodeTokens[i];
634+
if (filter(token)) {
635+
nodeTokensAfter.push(token);
636+
}
637+
if (nodeTokensAfter.length === count) {
638+
break;
639+
}
640+
}
641+
}
642+
}
643+
644+
return nodeTokensAfter;
569645
}
570-
/* oxlint-enable no-unused-vars */
571646

572647
/**
573648
* Get all of the tokens between two non-overlapping nodes.

apps/oxlint/test/tokens.test.ts

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ vi.mock('../src-js/plugins/source_code.ts', async (importOriginal) => {
2020
get sourceText() {
2121
return sourceText;
2222
},
23+
resetSourceAndAst() {
24+
// TODO: refactor this quick fix to get the tests working
25+
original.resetSourceAndAst();
26+
sourceText = '/*A*/var answer/*B*/=/*C*/a/*D*/* b/*E*///F\n call();\n/*Z*/';
27+
},
2328
};
2429
});
2530

@@ -305,11 +310,91 @@ describe('when calling getTokenAfter', () => {
305310
getTokenAfter;
306311
});
307312

313+
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L363-L459
308314
describe('when calling getTokensAfter', () => {
309-
/* oxlint-disable-next-line no-disabled-tests expect-expect */
310-
it('is to be implemented');
311-
/* oxlint-disable-next-line no-unused-expressions */
312-
getTokensAfter;
315+
it('should retrieve zero tokens after a node', () => {
316+
assert.deepStrictEqual(
317+
getTokensAfter(VariableDeclaratorIdentifier, 0).map((token) => token.value),
318+
[],
319+
);
320+
});
321+
322+
it('should retrieve one token after a node', () => {
323+
assert.deepStrictEqual(
324+
getTokensAfter(VariableDeclaratorIdentifier, 1).map((token) => token.value),
325+
['='],
326+
);
327+
});
328+
329+
it('should retrieve more than one token after a node', () => {
330+
assert.deepStrictEqual(
331+
getTokensAfter(VariableDeclaratorIdentifier, 2).map((token) => token.value),
332+
['=', 'a'],
333+
);
334+
});
335+
336+
it('should retrieve all tokens after a node', () => {
337+
assert.deepStrictEqual(
338+
getTokensAfter(VariableDeclaratorIdentifier, 9e9).map((token) => token.value),
339+
['=', 'a', '*', 'b', 'call', '(', ')', ';'],
340+
);
341+
});
342+
343+
it('should retrieve more than one token after a node with count option', () => {
344+
assert.deepStrictEqual(
345+
getTokensAfter(VariableDeclaratorIdentifier, { count: 2 }).map((token) => token.value),
346+
['=', 'a'],
347+
);
348+
});
349+
350+
it('should retrieve all matched tokens after a node with filter option', () => {
351+
assert.deepStrictEqual(
352+
getTokensAfter(VariableDeclaratorIdentifier, {
353+
filter: (t) => t.type === 'Identifier',
354+
}).map((token) => token.value),
355+
['a', 'b', 'call'],
356+
);
357+
});
358+
359+
it('should retrieve matched tokens after a node with count and filter options', () => {
360+
assert.deepStrictEqual(
361+
getTokensAfter(VariableDeclaratorIdentifier, {
362+
count: 2,
363+
filter: (t) => t.type === 'Identifier',
364+
}).map((token) => token.value),
365+
['a', 'b'],
366+
);
367+
});
368+
369+
it('should retrieve all tokens and comments after a node with includeComments option', () => {
370+
assert.deepStrictEqual(
371+
getTokensAfter(VariableDeclaratorIdentifier, {
372+
includeComments: true,
373+
}).map((token) => token.value),
374+
['B', '=', 'C', 'a', 'D', '*', 'b', 'E', 'F', 'call', '(', ')', ';', 'Z'],
375+
);
376+
});
377+
378+
it('should retrieve several tokens and comments after a node with includeComments and count options', () => {
379+
assert.deepStrictEqual(
380+
getTokensAfter(VariableDeclaratorIdentifier, {
381+
includeComments: true,
382+
count: 3,
383+
}).map((token) => token.value),
384+
['B', '=', 'C'],
385+
);
386+
});
387+
388+
it('should retrieve matched tokens and comments after a node with includeComments and count and filter options', () => {
389+
assert.deepStrictEqual(
390+
getTokensAfter(VariableDeclaratorIdentifier, {
391+
includeComments: true,
392+
count: 3,
393+
filter: (t) => t.type.startsWith('Block'),
394+
}).map((token) => token.value),
395+
['B', 'C', 'D'],
396+
);
397+
});
313398
});
314399

315400
describe('when calling getFirstTokens', () => {

0 commit comments

Comments
 (0)