Skip to content

Commit c0bd256

Browse files
committed
feat(linter/plugins): implement SourceCode#getFirstTokens()
1 parent ef05957 commit c0bd256

File tree

2 files changed

+181
-8
lines changed

2 files changed

+181
-8
lines changed

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

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { debugAssertIsNonNull } from '../utils/asserts.js';
99
import type { Comment, Node, NodeOrToken } from './types.ts';
1010
import type { Span } from './location.ts';
1111

12-
const { max } = Math;
12+
const { max, min } = Math;
1313

1414
/**
1515
* Options for various `SourceCode` methods e.g. `getFirstToken`.
@@ -284,11 +284,100 @@ export function getFirstToken(node: Node, skipOptions?: SkipOptions | number | F
284284
* If this is a function then it's `options.filter`.
285285
* @returns Array of `Token`s.
286286
*/
287-
/* oxlint-disable no-unused-vars */
288287
export function getFirstTokens(node: Node, countOptions?: CountOptions | number | FilterFn | null): Token[] {
289-
throw new Error('`sourceCode.getFirstTokens` not implemented yet'); // TODO
288+
if (tokens === null) initTokens();
289+
debugAssertIsNonNull(tokens);
290+
debugAssertIsNonNull(comments);
291+
292+
const count =
293+
typeof countOptions === 'number'
294+
? countOptions
295+
: typeof countOptions === 'object' && countOptions !== null
296+
? countOptions.count
297+
: null;
298+
299+
const filter =
300+
typeof countOptions === 'function'
301+
? countOptions
302+
: typeof countOptions === 'object' && countOptions !== null
303+
? countOptions.filter
304+
: null;
305+
306+
const includeComments =
307+
typeof countOptions === 'object' &&
308+
countOptions !== null &&
309+
'includeComments' in countOptions &&
310+
countOptions.includeComments;
311+
312+
let nodeTokens: Token[] | null = null;
313+
if (includeComments) {
314+
if (tokensWithComments === null) {
315+
tokensWithComments = [...tokens, ...comments].sort((a, b) => a.range[0] - b.range[0]);
316+
}
317+
nodeTokens = tokensWithComments;
318+
} else {
319+
nodeTokens = tokens;
320+
}
321+
322+
const { range } = node,
323+
rangeStart = range[0],
324+
rangeEnd = range[1];
325+
326+
// Binary search for first token within `node`'s range
327+
const tokensLength = nodeTokens.length;
328+
let sliceStart = tokensLength;
329+
for (let lo = 0; lo < sliceStart; ) {
330+
const mid = (lo + sliceStart) >> 1;
331+
if (nodeTokens[mid].range[0] < rangeStart) {
332+
lo = mid + 1;
333+
} else {
334+
sliceStart = mid;
335+
}
336+
}
337+
338+
// Binary search for the first token outside `node`'s range
339+
let sliceEnd = tokensLength;
340+
for (let lo = sliceStart; lo < sliceEnd; ) {
341+
const mid = (lo + sliceEnd) >> 1;
342+
if (nodeTokens[mid].range[0] < rangeEnd) {
343+
lo = mid + 1;
344+
} else {
345+
sliceEnd = mid;
346+
}
347+
}
348+
349+
let firstTokens: Token[];
350+
if (typeof filter !== 'function') {
351+
if (typeof count !== 'number') {
352+
firstTokens = nodeTokens.slice(sliceStart, sliceEnd);
353+
} else {
354+
firstTokens = nodeTokens.slice(sliceStart, min(sliceStart + count, sliceEnd));
355+
}
356+
} else {
357+
if (typeof count !== 'number') {
358+
firstTokens = [];
359+
for (let i = sliceStart; i < sliceEnd; i++) {
360+
const token = nodeTokens[i];
361+
if (filter(token)) {
362+
firstTokens.push(token);
363+
}
364+
}
365+
} else {
366+
firstTokens = [];
367+
for (let i = sliceStart; i < sliceEnd; i++) {
368+
const token = nodeTokens[i];
369+
if (filter(token)) {
370+
firstTokens.push(token);
371+
if (firstTokens.length === count) {
372+
break;
373+
}
374+
}
375+
}
376+
}
377+
}
378+
379+
return firstTokens;
290380
}
291-
/* oxlint-enable no-unused-vars */
292381

293382
/**
294383
* Get the last token of the given node.

apps/oxlint/test/tokens.test.ts

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,95 @@ describe('when calling getTokensAfter', () => {
312312
getTokensAfter;
313313
});
314314

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

322406
describe('when calling getFirstToken', () => {

0 commit comments

Comments
 (0)