Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 84 additions & 3 deletions apps/oxlint/src-js/plugins/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,14 +352,95 @@ export function getTokenOrCommentBefore(nodeOrToken: NodeOrToken | Comment, skip
* @param countOptions? - Options object. Same options as `getFirstTokens()`.
* @returns Array of `Token`s.
*/
/* oxlint-disable no-unused-vars */
export function getTokensBefore(
nodeOrToken: NodeOrToken | Comment,
countOptions?: CountOptions | number | FilterFn | null,
): Token[] {
throw new Error('`sourceCode.getTokensBefore` not implemented yet'); // TODO
if (tokens === null) initTokens();
debugAssertIsNonNull(tokens);
debugAssertIsNonNull(comments);

// Maximum number of tokens to return
const count =
typeof countOptions === 'number'
? max(0, countOptions)
: typeof countOptions === 'object' && countOptions !== null
? countOptions.count
: null;

// Function to filter tokens
const filter =
typeof countOptions === 'function'
? countOptions
: typeof countOptions === 'object' && countOptions !== null
? countOptions.filter
: null;

// Whether to return comment tokens
const includeComments =
typeof countOptions === 'object' &&
countOptions !== null &&
'includeComments' in countOptions &&
countOptions.includeComments;

// Source array of tokens to search in
let nodeTokens: Token[] | null = null;
if (includeComments) {
if (tokensWithComments === null) {
tokensWithComments = [...tokens, ...comments].sort((a, b) => a.range[0] - b.range[0]);
}
nodeTokens = tokensWithComments;
} else {
nodeTokens = tokens;
}

const targetStart = nodeOrToken.range[0];

let sliceEnd = 0;
let hi = nodeTokens.length;
while (sliceEnd < hi) {
const mid = (sliceEnd + hi) >> 1;
if (nodeTokens[mid].range[0] < targetStart) {
sliceEnd = mid + 1;
} else {
hi = mid;
}
}

let tokensBefore: Token[];
// Fast path for the common case
if (typeof filter !== 'function') {
if (typeof count !== 'number') {
tokensBefore = nodeTokens.slice(0, sliceEnd);
} else {
tokensBefore = nodeTokens.slice(sliceEnd - count, sliceEnd);
}
} else {
if (typeof count !== 'number') {
tokensBefore = [];
for (let i = 0; i < sliceEnd; i++) {
const token = nodeTokens[i];
if (filter(token)) {
tokensBefore.push(token);
}
}
} else {
tokensBefore = [];
// Count is the number of preceding tokens so we iterate in reverse
for (let i = sliceEnd - 1; i >= 0; i--) {
const token = nodeTokens[i];
if (filter(token)) {
tokensBefore.unshift(token);
}
if (tokensBefore.length === count) {
break;
}
}
}
}

return tokensBefore;
}
/* oxlint-enable no-unused-vars */

/**
* Get the token that follows a given node or token.
Expand Down
224 changes: 211 additions & 13 deletions apps/oxlint/test/tokens.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
import assert from 'node:assert';
import { describe, it, vi } from 'vitest';
import { getTokens } from '../src-js/plugins/tokens.js';
import {
getTokens,
getTokensBefore,
getTokenBefore,
getTokensAfter,
getTokenAfter,
getFirstTokens,
} from '../src-js/plugins/tokens.js';
import { resetSourceAndAst } from '../src-js/plugins/source_code.js';
import type { Node } from '../src-js/plugins/types.js';

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

vi.mock('../src-js/plugins/source_code.ts', () => {
vi.mock('../src-js/plugins/source_code.ts', async (importOriginal) => {
const original: any = await importOriginal();
return {
...original,
get sourceText() {
return sourceText;
},
};
});

// TODO: We are lying about `Program`'s range here.
// The range provided by `@typescript-eslint/typescript-estree` does not match the assertions for that of `espree`.
// The deviation is being corrected in upcoming releases of ESLint and TS-ESLint.
// https://eslint.org/blog/2025/10/whats-coming-in-eslint-10.0.0/#updates-to-program-ast-node-range-coverage
// https://github.com/typescript-eslint/typescript-eslint/issues/11026#issuecomment-3421887632
const Program = { range: [5, 55] } as Node;
const BinaryExpression = { range: [26, 35] } as Node;
/* oxlint-disable-next-line no-unused-vars */
const VariableDeclaratorIdentifier = { range: [9, 15] } as Node;

// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L62
describe('when calling getTokens', () => {
sourceText = '/*A*/var answer/*B*/=/*C*/a/*D*/* b/*E*///F\n call();\n/*Z*/';

// TODO: We are lying about `Program`'s range here.
// The range provided by `@typescript-eslint/typescript-estree` does not match the assertions for that of `espree`.
// The deviation is being corrected in upcoming releases of ESLint and TS-ESLint.
// https://eslint.org/blog/2025/10/whats-coming-in-eslint-10.0.0/#updates-to-program-ast-node-range-coverage
// https://github.com/typescript-eslint/typescript-eslint/issues/11026#issuecomment-3421887632
const Program = { range: [5, 55] } as Node;
const BinaryExpression = { range: [26, 35] } as Node;

it('should retrieve all tokens for root node', () => {
assert.deepStrictEqual(
getTokens(Program).map((token) => token.value),
Expand Down Expand Up @@ -104,3 +114,191 @@ describe('when calling getTokens', () => {
);
});
});

// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L157
describe('when calling getTokensBefore', () => {
it('should retrieve zero tokens before a node', () => {
assert.deepStrictEqual(
getTokensBefore(BinaryExpression, 0).map((token) => token.value),
[],
);
});

it('should retrieve one token before a node', () => {
assert.deepStrictEqual(
getTokensBefore(BinaryExpression, 1).map((token) => token.value),
['='],
);
});

it('should retrieve more than one token before a node', () => {
assert.deepStrictEqual(
getTokensBefore(BinaryExpression, 2).map((token) => token.value),
['answer', '='],
);
});

it('should retrieve all tokens before a node', () => {
assert.deepStrictEqual(
getTokensBefore(BinaryExpression, 9e9).map((token) => token.value),
['var', 'answer', '='],
);
});

it('should retrieve more than one token before a node with count option', () => {
assert.deepStrictEqual(
getTokensBefore(BinaryExpression, { count: 2 }).map((token) => token.value),
['answer', '='],
);
});

it('should retrieve matched tokens before a node with count and filter options', () => {
assert.deepStrictEqual(
getTokensBefore(BinaryExpression, {
count: 1,
filter: (t) => t.value !== '=',
}).map((token) => token.value),
['answer'],
);
});

it('should retrieve all matched tokens before a node with filter option', () => {
assert.deepStrictEqual(
getTokensBefore(BinaryExpression, {
filter: (t) => t.value !== 'answer',
}).map((token) => token.value),
['var', '='],
);
});

it('should retrieve no tokens before the root node', () => {
assert.deepStrictEqual(
getTokensBefore(Program, { count: 1 }).map((token) => token.value),
[],
);
});

it('should retrieve tokens and comments before a node with count and includeComments option', () => {
assert.deepStrictEqual(
getTokensBefore(BinaryExpression, {
count: 3,
includeComments: true,
}).map((token) => token.value),
['B', '=', 'C'],
);
});

it('should retrieve all tokens and comments before a node with includeComments option only', () => {
assert.deepStrictEqual(
getTokensBefore(BinaryExpression, {
includeComments: true,
}).map((token) => token.value),
['A', 'var', 'answer', 'B', '=', 'C'],
);
});

it('should retrieve all tokens and comments before a node with includeComments and filter options', () => {
assert.deepStrictEqual(
getTokensBefore(BinaryExpression, {
includeComments: true,
filter: (t) => t.type.startsWith('Block'),
}).map((token) => token.value),
['A', 'B', 'C'],
);
});
});

describe('when calling getTokenBefore', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
/* oxlint-disable-next-line no-unused-expressions */
getTokenBefore;
/* oxlint-disable-next-line no-unused-expressions */
resetSourceAndAst;
});

describe('when calling getTokenAfter', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
/* oxlint-disable-next-line no-unused-expressions */
getTokenAfter;
});

describe('when calling getTokensAfter', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
/* oxlint-disable-next-line no-unused-expressions */
getTokensAfter;
});

describe('when calling getFirstTokens', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
/* oxlint-disable-next-line no-unused-expressions */
getFirstTokens;
});

describe('when calling getFirstToken', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getLastTokens', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getLastToken', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getFirstTokensBetween', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getFirstTokenBetween', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getLastTokensBetween', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getLastTokenBetween', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getTokensBetween', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getTokenByRangeStart', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getTokenOrCommentBefore', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getTokenOrCommentAfter', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getFirstToken & getTokenAfter', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});

describe('when calling getLastToken & getTokenBefore', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
});
Loading