Skip to content

Commit 64766ef

Browse files
committed
feat(linter/plugins): implement SourceCode#getFirstTokens()
1 parent 2bfdd26 commit 64766ef

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
@@ -397,11 +397,95 @@ describe('when calling getTokensAfter', () => {
397397
});
398398
});
399399

400+
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L594-L673
400401
describe('when calling getFirstTokens', () => {
401-
/* oxlint-disable-next-line no-disabled-tests expect-expect */
402-
it('is to be implemented');
403-
/* oxlint-disable-next-line no-unused-expressions */
404-
getFirstTokens;
402+
it("should retrieve zero tokens from a node's token stream", () => {
403+
assert.deepStrictEqual(
404+
getFirstTokens(BinaryExpression, 0).map((token) => token.value),
405+
[],
406+
);
407+
});
408+
409+
it("should retrieve one token from a node's token stream", () => {
410+
assert.deepStrictEqual(
411+
getFirstTokens(BinaryExpression, 1).map((token) => token.value),
412+
['a'],
413+
);
414+
});
415+
416+
it("should retrieve more than one token from a node's token stream", () => {
417+
assert.deepStrictEqual(
418+
getFirstTokens(BinaryExpression, 2).map((token) => token.value),
419+
['a', '*'],
420+
);
421+
});
422+
423+
it("should retrieve all tokens from a node's token stream", () => {
424+
assert.deepStrictEqual(
425+
getFirstTokens(BinaryExpression, 9e9).map((token) => token.value),
426+
['a', '*', 'b'],
427+
);
428+
});
429+
430+
it("should retrieve more than one token from a node's token stream with count option", () => {
431+
assert.deepStrictEqual(
432+
getFirstTokens(BinaryExpression, { count: 2 }).map((token) => token.value),
433+
['a', '*'],
434+
);
435+
});
436+
437+
it("should retrieve matched tokens from a node's token stream with filter option", () => {
438+
assert.deepStrictEqual(
439+
getFirstTokens(BinaryExpression, (t) => t.type === 'Identifier').map((token) => token.value),
440+
['a', 'b'],
441+
);
442+
assert.deepStrictEqual(
443+
getFirstTokens(BinaryExpression, {
444+
filter: (t) => t.type === 'Identifier',
445+
}).map((token) => token.value),
446+
['a', 'b'],
447+
);
448+
});
449+
450+
it("should retrieve matched tokens from a node's token stream with filter and count options", () => {
451+
assert.deepStrictEqual(
452+
getFirstTokens(BinaryExpression, {
453+
count: 1,
454+
filter: (t) => t.type === 'Identifier',
455+
}).map((token) => token.value),
456+
['a'],
457+
);
458+
});
459+
460+
it("should retrieve all tokens and comments from a node's token stream with includeComments option", () => {
461+
assert.deepStrictEqual(
462+
getFirstTokens(BinaryExpression, {
463+
includeComments: true,
464+
}).map((token) => token.value),
465+
['a', 'D', '*', 'b'],
466+
);
467+
});
468+
469+
it("should retrieve several tokens and comments from a node's token stream with includeComments and count options", () => {
470+
assert.deepStrictEqual(
471+
getFirstTokens(BinaryExpression, {
472+
includeComments: true,
473+
count: 3,
474+
}).map((token) => token.value),
475+
['a', 'D', '*'],
476+
);
477+
});
478+
479+
it("should retrieve several tokens and comments from a node's token stream with includeComments and count and filter options", () => {
480+
assert.deepStrictEqual(
481+
getFirstTokens(BinaryExpression, {
482+
includeComments: true,
483+
count: 3,
484+
filter: (t) => t.value !== 'a',
485+
}).map((token) => token.value),
486+
['D', '*', 'b'],
487+
);
488+
});
405489
});
406490

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

0 commit comments

Comments
 (0)