Skip to content

Commit f2c35db

Browse files
committed
feat(linter/plugins): implement SourceCode#getTokenBefore()
1 parent 169b01f commit f2c35db

File tree

2 files changed

+172
-7
lines changed

2 files changed

+172
-7
lines changed

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

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,91 @@ export function getTokenBefore(
324324
nodeOrToken: NodeOrToken | Comment,
325325
skipOptions?: SkipOptions | number | FilterFn | null,
326326
): Token | null {
327-
throw new Error('`sourceCode.getTokenBefore` not implemented yet'); // TODO
327+
if (tokens === null) initTokens();
328+
debugAssertIsNonNull(tokens);
329+
debugAssertIsNonNull(comments);
330+
331+
// Number of tokens preceding the given node to skip
332+
let skip =
333+
typeof skipOptions === 'number'
334+
? skipOptions
335+
: typeof skipOptions === 'object' && skipOptions !== null
336+
? skipOptions.skip
337+
: null;
338+
339+
const filter =
340+
typeof skipOptions === 'function'
341+
? skipOptions
342+
: typeof skipOptions === 'object' && skipOptions !== null
343+
? skipOptions.filter
344+
: null;
345+
346+
// Whether to return comment tokens
347+
const includeComments =
348+
typeof skipOptions === 'object' &&
349+
skipOptions !== null &&
350+
'includeComments' in skipOptions &&
351+
skipOptions.includeComments;
352+
353+
// Source array of tokens to search in
354+
let nodeTokens: Token[] | null = null;
355+
if (includeComments) {
356+
if (tokensWithComments === null) {
357+
// TODO: `tokens` and `comments` are already sorted, so there's a more efficient algorithm to merge them.
358+
// That'd certainly be faster in Rust, but maybe here it's faster to leave it to JS engine to sort them?
359+
// TODO: Once we have our own tokens which have `start` and `end` properties, we can use them instead of `range`.
360+
tokensWithComments = [...tokens, ...comments].sort((a, b) => a.range[0] - b.range[0]);
361+
}
362+
nodeTokens = tokensWithComments;
363+
} else {
364+
nodeTokens = tokens;
365+
}
366+
367+
const nodeStart = nodeOrToken.range[0];
368+
369+
// Index of the token immediately before the given node, token, or comment.
370+
let beforeIndex = 0;
371+
let hi = nodeTokens.length;
372+
373+
while (beforeIndex < hi) {
374+
const mid = (beforeIndex + hi) >> 1;
375+
if (nodeTokens[mid].range[0] < nodeStart) {
376+
beforeIndex = mid + 1;
377+
} else {
378+
hi = mid;
379+
}
380+
}
381+
382+
beforeIndex -= 1;
383+
384+
if (typeof filter !== 'function') {
385+
if (typeof skip !== 'number') {
386+
return nodeTokens[beforeIndex] ?? null;
387+
} else {
388+
return nodeTokens[beforeIndex - skip] ?? null;
389+
}
390+
} else {
391+
if (typeof skip !== 'number') {
392+
while (beforeIndex >= 0) {
393+
const token = nodeTokens[beforeIndex];
394+
if (filter(token)) {
395+
return token;
396+
}
397+
beforeIndex -= 1;
398+
}
399+
} else {
400+
while (beforeIndex >= 0) {
401+
const token = nodeTokens[beforeIndex];
402+
if (filter(token)) {
403+
if (skip === 0) return token;
404+
skip -= 1;
405+
}
406+
beforeIndex -= 1;
407+
}
408+
}
409+
}
410+
411+
return null;
328412
}
329413
/* oxlint-enable no-unused-vars */
330414

apps/oxlint/test/tokens.test.ts

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

126126
describe('when calling getTokenBefore', () => {
127-
/* oxlint-disable-next-line no-disabled-tests expect-expect */
128-
it('is to be implemented');
129-
/* oxlint-disable-next-line no-unused-expressions */
130-
getTokenBefore;
131-
/* oxlint-disable-next-line no-unused-expressions */
132-
resetSourceAndAst;
127+
it('should retrieve one token before a node', () => {
128+
assert.strictEqual(getTokenBefore(BinaryExpression)!.value, '=');
129+
});
130+
131+
it('should skip a given number of tokens', () => {
132+
assert.strictEqual(getTokenBefore(BinaryExpression, 1)!.value, 'answer');
133+
assert.strictEqual(getTokenBefore(BinaryExpression, 2)!.value, 'var');
134+
});
135+
136+
it('should skip a given number of tokens with skip option', () => {
137+
assert.strictEqual(getTokenBefore(BinaryExpression, { skip: 1 })!.value, 'answer');
138+
assert.strictEqual(getTokenBefore(BinaryExpression, { skip: 2 })!.value, 'var');
139+
});
140+
141+
it('should retrieve matched token with filter option', () => {
142+
assert.strictEqual(getTokenBefore(BinaryExpression, (t) => t.value !== '=')!.value, 'answer');
143+
});
144+
145+
it('should retrieve matched token with skip and filter options', () => {
146+
assert.strictEqual(
147+
getTokenBefore(BinaryExpression, {
148+
skip: 1,
149+
filter: (t) => t.value !== '=',
150+
})!.value,
151+
'var',
152+
);
153+
});
154+
155+
it('should retrieve one token or comment before a node with includeComments option', () => {
156+
assert.strictEqual(
157+
getTokenBefore(BinaryExpression, {
158+
includeComments: true,
159+
})!.value,
160+
'C',
161+
);
162+
});
163+
164+
it('should retrieve one token or comment before a node with includeComments and skip options', () => {
165+
assert.strictEqual(
166+
getTokenBefore(BinaryExpression, {
167+
includeComments: true,
168+
skip: 1,
169+
})!.value,
170+
'=',
171+
);
172+
});
173+
174+
it('should retrieve one token or comment before a node with includeComments and skip and filter options', () => {
175+
assert.strictEqual(
176+
getTokenBefore(BinaryExpression, {
177+
includeComments: true,
178+
skip: 1,
179+
filter: (t) => t.type.startsWith('Block'),
180+
})!.value,
181+
'B',
182+
);
183+
});
184+
185+
it('should retrieve the previous node if the comment at the end of source code is specified.', () => {
186+
resetSourceAndAst();
187+
sourceText = 'a + b /*comment*/';
188+
// TODO: this verbatim range should be replaced with `ast.comments[0]`
189+
const token = getTokenBefore({ range: [6, 17] } as Node);
190+
191+
assert.strictEqual(token!.value, 'b');
192+
resetSourceAndAst();
193+
});
194+
195+
it('should retrieve the previous comment if the first token is specified.', () => {
196+
resetSourceAndAst();
197+
sourceText = '/*comment*/ a + b';
198+
// TODO: this verbatim range should be replaced with `ast.tokens[0]`
199+
const token = getTokenBefore({ range: [12, 13] } as Node, { includeComments: true });
200+
201+
assert.strictEqual(token!.value, 'comment');
202+
resetSourceAndAst();
203+
});
204+
205+
it('should retrieve null if the first comment is specified.', () => {
206+
resetSourceAndAst();
207+
sourceText = '/*comment*/ a + b';
208+
// TODO: this verbatim range should be replaced with `ast.comments[0]`
209+
const token = getTokenBefore({ range: [0, 11] } as Node, { includeComments: true });
210+
211+
assert.strictEqual(token, null);
212+
resetSourceAndAst();
213+
});
133214
});
134215

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

0 commit comments

Comments
 (0)