Skip to content

Commit 776e473

Browse files
authored
feat(linter/plugins): implement SourceCode#getTokensBefore() (#15955)
- Part of #14829 (comment). - Follow up to #15861.
1 parent 595867a commit 776e473

File tree

2 files changed

+295
-16
lines changed

2 files changed

+295
-16
lines changed

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

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -352,14 +352,95 @@ export function getTokenOrCommentBefore(nodeOrToken: NodeOrToken | Comment, skip
352352
* @param countOptions? - Options object. Same options as `getFirstTokens()`.
353353
* @returns Array of `Token`s.
354354
*/
355-
/* oxlint-disable no-unused-vars */
356355
export function getTokensBefore(
357356
nodeOrToken: NodeOrToken | Comment,
358357
countOptions?: CountOptions | number | FilterFn | null,
359358
): Token[] {
360-
throw new Error('`sourceCode.getTokensBefore` not implemented yet'); // TODO
359+
if (tokens === null) initTokens();
360+
debugAssertIsNonNull(tokens);
361+
debugAssertIsNonNull(comments);
362+
363+
// Maximum number of tokens to return
364+
const count =
365+
typeof countOptions === 'number'
366+
? max(0, countOptions)
367+
: typeof countOptions === 'object' && countOptions !== null
368+
? countOptions.count
369+
: null;
370+
371+
// Function to filter tokens
372+
const filter =
373+
typeof countOptions === 'function'
374+
? countOptions
375+
: typeof countOptions === 'object' && countOptions !== null
376+
? countOptions.filter
377+
: null;
378+
379+
// Whether to return comment tokens
380+
const includeComments =
381+
typeof countOptions === 'object' &&
382+
countOptions !== null &&
383+
'includeComments' in countOptions &&
384+
countOptions.includeComments;
385+
386+
// Source array of tokens to search in
387+
let nodeTokens: Token[] | null = null;
388+
if (includeComments) {
389+
if (tokensWithComments === null) {
390+
tokensWithComments = [...tokens, ...comments].sort((a, b) => a.range[0] - b.range[0]);
391+
}
392+
nodeTokens = tokensWithComments;
393+
} else {
394+
nodeTokens = tokens;
395+
}
396+
397+
const targetStart = nodeOrToken.range[0];
398+
399+
let sliceEnd = 0;
400+
let hi = nodeTokens.length;
401+
while (sliceEnd < hi) {
402+
const mid = (sliceEnd + hi) >> 1;
403+
if (nodeTokens[mid].range[0] < targetStart) {
404+
sliceEnd = mid + 1;
405+
} else {
406+
hi = mid;
407+
}
408+
}
409+
410+
let tokensBefore: Token[];
411+
// Fast path for the common case
412+
if (typeof filter !== 'function') {
413+
if (typeof count !== 'number') {
414+
tokensBefore = nodeTokens.slice(0, sliceEnd);
415+
} else {
416+
tokensBefore = nodeTokens.slice(sliceEnd - count, sliceEnd);
417+
}
418+
} else {
419+
if (typeof count !== 'number') {
420+
tokensBefore = [];
421+
for (let i = 0; i < sliceEnd; i++) {
422+
const token = nodeTokens[i];
423+
if (filter(token)) {
424+
tokensBefore.push(token);
425+
}
426+
}
427+
} else {
428+
tokensBefore = [];
429+
// Count is the number of preceding tokens so we iterate in reverse
430+
for (let i = sliceEnd - 1; i >= 0; i--) {
431+
const token = nodeTokens[i];
432+
if (filter(token)) {
433+
tokensBefore.unshift(token);
434+
}
435+
if (tokensBefore.length === count) {
436+
break;
437+
}
438+
}
439+
}
440+
}
441+
442+
return tokensBefore;
361443
}
362-
/* oxlint-enable no-unused-vars */
363444

364445
/**
365446
* Get the token that follows a given node or token.

apps/oxlint/test/tokens.test.ts

Lines changed: 211 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,40 @@
11
import assert from 'node:assert';
22
import { describe, it, vi } from 'vitest';
3-
import { getTokens } from '../src-js/plugins/tokens.js';
3+
import {
4+
getTokens,
5+
getTokensBefore,
6+
getTokenBefore,
7+
getTokensAfter,
8+
getTokenAfter,
9+
getFirstTokens,
10+
} from '../src-js/plugins/tokens.js';
11+
import { resetSourceAndAst } from '../src-js/plugins/source_code.js';
412
import type { Node } from '../src-js/plugins/types.js';
513

6-
let sourceText = 'null;';
14+
let sourceText = '/*A*/var answer/*B*/=/*C*/a/*D*/* b/*E*///F\n call();\n/*Z*/';
715

8-
vi.mock('../src-js/plugins/source_code.ts', () => {
16+
vi.mock('../src-js/plugins/source_code.ts', async (importOriginal) => {
17+
const original: any = await importOriginal();
918
return {
19+
...original,
1020
get sourceText() {
1121
return sourceText;
1222
},
1323
};
1424
});
1525

26+
// TODO: We are lying about `Program`'s range here.
27+
// The range provided by `@typescript-eslint/typescript-estree` does not match the assertions for that of `espree`.
28+
// The deviation is being corrected in upcoming releases of ESLint and TS-ESLint.
29+
// https://eslint.org/blog/2025/10/whats-coming-in-eslint-10.0.0/#updates-to-program-ast-node-range-coverage
30+
// https://github.com/typescript-eslint/typescript-eslint/issues/11026#issuecomment-3421887632
31+
const Program = { range: [5, 55] } as Node;
32+
const BinaryExpression = { range: [26, 35] } as Node;
33+
/* oxlint-disable-next-line no-unused-vars */
34+
const VariableDeclaratorIdentifier = { range: [9, 15] } as Node;
35+
1636
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L62
1737
describe('when calling getTokens', () => {
18-
sourceText = '/*A*/var answer/*B*/=/*C*/a/*D*/* b/*E*///F\n call();\n/*Z*/';
19-
20-
// TODO: We are lying about `Program`'s range here.
21-
// The range provided by `@typescript-eslint/typescript-estree` does not match the assertions for that of `espree`.
22-
// The deviation is being corrected in upcoming releases of ESLint and TS-ESLint.
23-
// https://eslint.org/blog/2025/10/whats-coming-in-eslint-10.0.0/#updates-to-program-ast-node-range-coverage
24-
// https://github.com/typescript-eslint/typescript-eslint/issues/11026#issuecomment-3421887632
25-
const Program = { range: [5, 55] } as Node;
26-
const BinaryExpression = { range: [26, 35] } as Node;
27-
2838
it('should retrieve all tokens for root node', () => {
2939
assert.deepStrictEqual(
3040
getTokens(Program).map((token) => token.value),
@@ -104,3 +114,191 @@ describe('when calling getTokens', () => {
104114
);
105115
});
106116
});
117+
118+
// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L157
119+
describe('when calling getTokensBefore', () => {
120+
it('should retrieve zero tokens before a node', () => {
121+
assert.deepStrictEqual(
122+
getTokensBefore(BinaryExpression, 0).map((token) => token.value),
123+
[],
124+
);
125+
});
126+
127+
it('should retrieve one token before a node', () => {
128+
assert.deepStrictEqual(
129+
getTokensBefore(BinaryExpression, 1).map((token) => token.value),
130+
['='],
131+
);
132+
});
133+
134+
it('should retrieve more than one token before a node', () => {
135+
assert.deepStrictEqual(
136+
getTokensBefore(BinaryExpression, 2).map((token) => token.value),
137+
['answer', '='],
138+
);
139+
});
140+
141+
it('should retrieve all tokens before a node', () => {
142+
assert.deepStrictEqual(
143+
getTokensBefore(BinaryExpression, 9e9).map((token) => token.value),
144+
['var', 'answer', '='],
145+
);
146+
});
147+
148+
it('should retrieve more than one token before a node with count option', () => {
149+
assert.deepStrictEqual(
150+
getTokensBefore(BinaryExpression, { count: 2 }).map((token) => token.value),
151+
['answer', '='],
152+
);
153+
});
154+
155+
it('should retrieve matched tokens before a node with count and filter options', () => {
156+
assert.deepStrictEqual(
157+
getTokensBefore(BinaryExpression, {
158+
count: 1,
159+
filter: (t) => t.value !== '=',
160+
}).map((token) => token.value),
161+
['answer'],
162+
);
163+
});
164+
165+
it('should retrieve all matched tokens before a node with filter option', () => {
166+
assert.deepStrictEqual(
167+
getTokensBefore(BinaryExpression, {
168+
filter: (t) => t.value !== 'answer',
169+
}).map((token) => token.value),
170+
['var', '='],
171+
);
172+
});
173+
174+
it('should retrieve no tokens before the root node', () => {
175+
assert.deepStrictEqual(
176+
getTokensBefore(Program, { count: 1 }).map((token) => token.value),
177+
[],
178+
);
179+
});
180+
181+
it('should retrieve tokens and comments before a node with count and includeComments option', () => {
182+
assert.deepStrictEqual(
183+
getTokensBefore(BinaryExpression, {
184+
count: 3,
185+
includeComments: true,
186+
}).map((token) => token.value),
187+
['B', '=', 'C'],
188+
);
189+
});
190+
191+
it('should retrieve all tokens and comments before a node with includeComments option only', () => {
192+
assert.deepStrictEqual(
193+
getTokensBefore(BinaryExpression, {
194+
includeComments: true,
195+
}).map((token) => token.value),
196+
['A', 'var', 'answer', 'B', '=', 'C'],
197+
);
198+
});
199+
200+
it('should retrieve all tokens and comments before a node with includeComments and filter options', () => {
201+
assert.deepStrictEqual(
202+
getTokensBefore(BinaryExpression, {
203+
includeComments: true,
204+
filter: (t) => t.type.startsWith('Block'),
205+
}).map((token) => token.value),
206+
['A', 'B', 'C'],
207+
);
208+
});
209+
});
210+
211+
describe('when calling getTokenBefore', () => {
212+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
213+
it('is to be implemented');
214+
/* oxlint-disable-next-line no-unused-expressions */
215+
getTokenBefore;
216+
/* oxlint-disable-next-line no-unused-expressions */
217+
resetSourceAndAst;
218+
});
219+
220+
describe('when calling getTokenAfter', () => {
221+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
222+
it('is to be implemented');
223+
/* oxlint-disable-next-line no-unused-expressions */
224+
getTokenAfter;
225+
});
226+
227+
describe('when calling getTokensAfter', () => {
228+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
229+
it('is to be implemented');
230+
/* oxlint-disable-next-line no-unused-expressions */
231+
getTokensAfter;
232+
});
233+
234+
describe('when calling getFirstTokens', () => {
235+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
236+
it('is to be implemented');
237+
/* oxlint-disable-next-line no-unused-expressions */
238+
getFirstTokens;
239+
});
240+
241+
describe('when calling getFirstToken', () => {
242+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
243+
it('is to be implemented');
244+
});
245+
246+
describe('when calling getLastTokens', () => {
247+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
248+
it('is to be implemented');
249+
});
250+
251+
describe('when calling getLastToken', () => {
252+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
253+
it('is to be implemented');
254+
});
255+
256+
describe('when calling getFirstTokensBetween', () => {
257+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
258+
it('is to be implemented');
259+
});
260+
261+
describe('when calling getFirstTokenBetween', () => {
262+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
263+
it('is to be implemented');
264+
});
265+
266+
describe('when calling getLastTokensBetween', () => {
267+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
268+
it('is to be implemented');
269+
});
270+
271+
describe('when calling getLastTokenBetween', () => {
272+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
273+
it('is to be implemented');
274+
});
275+
276+
describe('when calling getTokensBetween', () => {
277+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
278+
it('is to be implemented');
279+
});
280+
281+
describe('when calling getTokenByRangeStart', () => {
282+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
283+
it('is to be implemented');
284+
});
285+
286+
describe('when calling getTokenOrCommentBefore', () => {
287+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
288+
it('is to be implemented');
289+
});
290+
291+
describe('when calling getTokenOrCommentAfter', () => {
292+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
293+
it('is to be implemented');
294+
});
295+
296+
describe('when calling getFirstToken & getTokenAfter', () => {
297+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
298+
it('is to be implemented');
299+
});
300+
301+
describe('when calling getLastToken & getTokenBefore', () => {
302+
/* oxlint-disable-next-line no-disabled-tests expect-expect */
303+
it('is to be implemented');
304+
});

0 commit comments

Comments
 (0)