Skip to content

Commit 45fffc1

Browse files
authored
feat(linter/plugins): implement SourceCode#getTokenBefore() (#15956)
- Part of #14829 (comment). - Follow up to #15861.
1 parent 6013e68 commit 45fffc1

File tree

2 files changed

+169
-9
lines changed

2 files changed

+169
-9
lines changed

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

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,14 +319,93 @@ export function getLastTokens(node: Node, countOptions?: CountOptions | number |
319319
* @param skipOptions? - Options object. Same options as `getFirstToken()`.
320320
* @returns `Token`, or `null` if all were skipped.
321321
*/
322-
/* oxlint-disable no-unused-vars */
323322
export function getTokenBefore(
324323
nodeOrToken: NodeOrToken | Comment,
325324
skipOptions?: SkipOptions | number | FilterFn | null,
326325
): Token | null {
327-
throw new Error('`sourceCode.getTokenBefore` not implemented yet'); // TODO
326+
if (tokens === null) initTokens();
327+
debugAssertIsNonNull(tokens);
328+
debugAssertIsNonNull(comments);
329+
330+
// Number of tokens preceding the given node to skip
331+
let skip =
332+
typeof skipOptions === 'number'
333+
? skipOptions
334+
: typeof skipOptions === 'object' && skipOptions !== null
335+
? skipOptions.skip
336+
: null;
337+
338+
const filter =
339+
typeof skipOptions === 'function'
340+
? skipOptions
341+
: typeof skipOptions === 'object' && skipOptions !== null
342+
? skipOptions.filter
343+
: null;
344+
345+
// Whether to return comment tokens
346+
const includeComments =
347+
typeof skipOptions === 'object' &&
348+
skipOptions !== null &&
349+
'includeComments' in skipOptions &&
350+
skipOptions.includeComments;
351+
352+
// Source array of tokens to search in
353+
let nodeTokens: Token[] | null = null;
354+
if (includeComments) {
355+
if (tokensWithComments === null) {
356+
tokensWithComments = [...tokens, ...comments].sort((a, b) => a.range[0] - b.range[0]);
357+
}
358+
nodeTokens = tokensWithComments;
359+
} else {
360+
nodeTokens = tokens;
361+
}
362+
363+
const nodeStart = nodeOrToken.range[0];
364+
365+
// Index of the token immediately before the given node, token, or comment.
366+
let beforeIndex = 0;
367+
let hi = nodeTokens.length;
368+
369+
while (beforeIndex < hi) {
370+
const mid = (beforeIndex + hi) >> 1;
371+
if (nodeTokens[mid].range[0] < nodeStart) {
372+
beforeIndex = mid + 1;
373+
} else {
374+
hi = mid;
375+
}
376+
}
377+
378+
beforeIndex -= 1;
379+
380+
if (typeof filter !== 'function') {
381+
if (typeof skip !== 'number') {
382+
return nodeTokens[beforeIndex] ?? null;
383+
} else {
384+
return nodeTokens[beforeIndex - skip] ?? null;
385+
}
386+
} else {
387+
if (typeof skip !== 'number') {
388+
while (beforeIndex >= 0) {
389+
const token = nodeTokens[beforeIndex];
390+
if (filter(token)) {
391+
return token;
392+
}
393+
beforeIndex -= 1;
394+
}
395+
} else {
396+
while (beforeIndex >= 0) {
397+
const token = nodeTokens[beforeIndex];
398+
if (filter(token)) {
399+
if (skip === 0) return token;
400+
skip -= 1;
401+
}
402+
beforeIndex -= 1;
403+
}
404+
}
405+
}
406+
407+
return null;
328408
}
329-
/* oxlint-enable no-unused-vars */
330409

331410
/**
332411
* Get the token that precedes a given node or token.

apps/oxlint/test/tokens.test.ts

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,93 @@ describe('when calling getTokensBefore', () => {
209209
});
210210

211211
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;
212+
it('should retrieve one token before a node', () => {
213+
assert.strictEqual(getTokenBefore(BinaryExpression)!.value, '=');
214+
});
215+
216+
it('should skip a given number of tokens', () => {
217+
assert.strictEqual(getTokenBefore(BinaryExpression, 1)!.value, 'answer');
218+
assert.strictEqual(getTokenBefore(BinaryExpression, 2)!.value, 'var');
219+
});
220+
221+
it('should skip a given number of tokens with skip option', () => {
222+
assert.strictEqual(getTokenBefore(BinaryExpression, { skip: 1 })!.value, 'answer');
223+
assert.strictEqual(getTokenBefore(BinaryExpression, { skip: 2 })!.value, 'var');
224+
});
225+
226+
it('should retrieve matched token with filter option', () => {
227+
assert.strictEqual(getTokenBefore(BinaryExpression, (t) => t.value !== '=')!.value, 'answer');
228+
});
229+
230+
it('should retrieve matched token with skip and filter options', () => {
231+
assert.strictEqual(
232+
getTokenBefore(BinaryExpression, {
233+
skip: 1,
234+
filter: (t) => t.value !== '=',
235+
})!.value,
236+
'var',
237+
);
238+
});
239+
240+
it('should retrieve one token or comment before a node with includeComments option', () => {
241+
assert.strictEqual(
242+
getTokenBefore(BinaryExpression, {
243+
includeComments: true,
244+
})!.value,
245+
'C',
246+
);
247+
});
248+
249+
it('should retrieve one token or comment before a node with includeComments and skip options', () => {
250+
assert.strictEqual(
251+
getTokenBefore(BinaryExpression, {
252+
includeComments: true,
253+
skip: 1,
254+
})!.value,
255+
'=',
256+
);
257+
});
258+
259+
it('should retrieve one token or comment before a node with includeComments and skip and filter options', () => {
260+
assert.strictEqual(
261+
getTokenBefore(BinaryExpression, {
262+
includeComments: true,
263+
skip: 1,
264+
filter: (t) => t.type.startsWith('Block'),
265+
})!.value,
266+
'B',
267+
);
268+
});
269+
270+
it('should retrieve the previous node if the comment at the end of source code is specified.', () => {
271+
resetSourceAndAst();
272+
sourceText = 'a + b /*comment*/';
273+
// TODO: this verbatim range should be replaced with `ast.comments[0]`
274+
const token = getTokenBefore({ range: [6, 17] } as Node);
275+
276+
assert.strictEqual(token!.value, 'b');
277+
resetSourceAndAst();
278+
});
279+
280+
it('should retrieve the previous comment if the first token is specified.', () => {
281+
resetSourceAndAst();
282+
sourceText = '/*comment*/ a + b';
283+
// TODO: this verbatim range should be replaced with `ast.tokens[0]`
284+
const token = getTokenBefore({ range: [12, 13] } as Node, { includeComments: true });
285+
286+
assert.strictEqual(token!.value, 'comment');
287+
resetSourceAndAst();
288+
});
289+
290+
it('should retrieve null if the first comment is specified.', () => {
291+
resetSourceAndAst();
292+
sourceText = '/*comment*/ a + b';
293+
// TODO: this verbatim range should be replaced with `ast.comments[0]`
294+
const token = getTokenBefore({ range: [0, 11] } as Node, { includeComments: true });
295+
296+
assert.strictEqual(token, null);
297+
resetSourceAndAst();
298+
});
218299
});
219300

220301
describe('when calling getTokenAfter', () => {

0 commit comments

Comments
 (0)