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
202 changes: 185 additions & 17 deletions apps/oxlint/src-js/plugins/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1129,24 +1129,103 @@ export function getTokensBetween(

/**
* Get the first token between two non-overlapping nodes.
* @param nodeOrToken1 - Node before the desired token range.
* @param nodeOrToken2 - Node after the desired token range.
* @param left - Node or token before the desired token range.
* @param right - Node or token after the desired token range.
* @param skipOptions? - Options object.
* If is a number, equivalent to `{ skip: n }`.
* If is a function, equivalent to `{ filter: fn }`.
* @returns `Token`, or `null` if all were skipped.
*/
/* oxlint-disable no-unused-vars */
export function getFirstTokenBetween(
nodeOrToken1: NodeOrToken | Comment,
nodeOrToken2: NodeOrToken | Comment,
left: NodeOrToken | Comment,
right: NodeOrToken | Comment,
skipOptions?: SkipOptions | number | FilterFn | null,
): Token | null {
// TODO: Check that `skipOptions` being a number or a function is supported by ESLint in this method.
// Original type def was `SkipOptions | null`. I (@overlookmotel) assume that was a mistake.
throw new Error('`sourceCode.getFirstTokenBetween` not implemented yet'); // TODO
if (tokens === null) initTokens();
debugAssertIsNonNull(tokens);
debugAssertIsNonNull(comments);

let skip =
typeof skipOptions === 'number'
? skipOptions
: typeof skipOptions === 'object' && skipOptions !== null
? skipOptions.skip
: null;

const filter =
typeof skipOptions === 'function'
? skipOptions
: typeof skipOptions === 'object' && skipOptions !== null
? skipOptions.filter
: null;

const includeComments =
typeof skipOptions === 'object' &&
skipOptions !== null &&
'includeComments' in skipOptions &&
skipOptions.includeComments;

let nodeTokens: Token[] | null = null;
if (includeComments) {
if (tokensWithComments === null) initTokensWithComments();
debugAssertIsNonNull(tokensWithComments);
nodeTokens = tokensWithComments;
} else {
nodeTokens = tokens;
}

// This range is not invariant over node order.
// The first argument must be the left node.
// Same as ESLint's implementation.
const rangeStart = left.range[1],
rangeEnd = right.range[0];

const tokensLength = nodeTokens.length;

// Binary search for the token following the left node
let firstTokenIndex = tokensLength;
for (let lo = 0; lo < firstTokenIndex; ) {
const mid = (lo + firstTokenIndex) >> 1;
if (nodeTokens[mid].range[0] < rangeStart) {
lo = mid + 1;
} else {
firstTokenIndex = mid;
}
}

if (typeof filter !== 'function') {
const token = nodeTokens[typeof skip === 'number' ? firstTokenIndex + skip : firstTokenIndex];
if (token === undefined || token.range[0] >= rangeEnd) return null;
return token;
} else {
if (typeof skip !== 'number') {
for (let i = firstTokenIndex; i < tokensLength; i++) {
const token = nodeTokens[i];
if (token.range[0] >= rangeEnd) {
break;
}
if (filter(token)) {
return token;
}
}
} else {
for (let i = firstTokenIndex; i < tokensLength; i++) {
const token = nodeTokens[i];
if (token.range[0] >= rangeEnd) {
break;
}
if (filter(token)) {
if (skip <= 0) {
return token;
}
skip--;
}
}
}
}

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

/**
* Get the first tokens between two non-overlapping nodes.
Expand Down Expand Up @@ -1360,22 +1439,111 @@ export function getLastTokenBetween(

/**
* Get the last tokens between two non-overlapping nodes.
* @param nodeOrToken1 - Node before the desired token range.
* @param nodeOrToken2 - Node after the desired token range.
* @param left - Node or token before the desired token range.
* @param right - Node or token after the desired token range.
* @param countOptions? - Options object.
* If is a number, equivalent to `{ count: n }`.
* If is a function, equivalent to `{ filter: fn }`.
* @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`.
* @returns Array of `Token`s between `left` and `right`.
*/
/* oxlint-disable no-unused-vars */
export function getLastTokensBetween(
nodeOrToken1: NodeOrToken | Comment,
nodeOrToken2: NodeOrToken | Comment,
left: NodeOrToken | Comment,
right: NodeOrToken | Comment,
countOptions?: CountOptions | number | FilterFn | null,
): Token[] {
throw new Error('`sourceCode.getLastTokensBetween` not implemented yet'); // TODO
if (tokens === null) initTokens();
debugAssertIsNonNull(tokens);
debugAssertIsNonNull(comments);

const count =
typeof countOptions === 'number'
? countOptions
: typeof countOptions === 'object' && countOptions !== null
? countOptions.count
: null;

const filter =
typeof countOptions === 'function'
? countOptions
: typeof countOptions === 'object' && countOptions !== null
? countOptions.filter
: null;

const includeComments =
typeof countOptions === 'object' &&
countOptions !== null &&
'includeComments' in countOptions &&
countOptions.includeComments;

let nodeTokens: Token[] | null = null;
if (includeComments) {
if (tokensWithComments === null) initTokensWithComments();
debugAssertIsNonNull(tokensWithComments);
nodeTokens = tokensWithComments;
} else {
nodeTokens = tokens;
}

// This range is not invariant over node order.
// The first argument must be the left node.
// Same as ESLint's implementation.
const rangeStart = left.range[1],
rangeEnd = right.range[0],
tokensLength = nodeTokens.length;

// Binary search for first token past the beginning of the `between` range
let sliceStart = tokensLength;
for (let lo = 0; lo < sliceStart; ) {
const mid = (lo + sliceStart) >> 1;
if (nodeTokens[mid].range[0] < rangeStart) {
lo = mid + 1;
} else {
sliceStart = mid;
}
}

// Binary search for first token past the end of the `between` range
let sliceEnd = tokensLength;
for (let lo = sliceStart; lo < sliceEnd; ) {
const mid = (lo + sliceEnd) >> 1;
if (nodeTokens[mid].range[0] < rangeEnd) {
lo = mid + 1;
} else {
sliceEnd = mid;
}
}

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

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

/**
* Get the token starting at the specified index.
Expand Down
112 changes: 104 additions & 8 deletions apps/oxlint/test/tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -994,18 +994,114 @@ describe('when calling getFirstTokensBetween', () => {
});
});

// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L1193-L1296
describe('when calling getFirstTokenBetween', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
/* oxlint-disable-next-line no-unused-expressions */
getFirstTokenBetween;
it('should return null between adjacent nodes', () => {
expect(getFirstTokenBetween(BinaryExpression, CallExpression)).toBeNull();
});

it('should retrieve one token between non-adjacent nodes', () => {
expect(getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right)!.value).toEqual('=');
});

it('should retrieve one token between non-adjacent nodes with skip option', () => {
expect(getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, 1)!.value).toEqual('a');
expect(getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { skip: 2 })!.value).toEqual('*');
});

it("should return null if it's skipped beyond the right token", () => {
expect(getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { skip: 3 })).toBeNull();
expect(getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { skip: 4 })).toBeNull();
});

it('should retrieve the first matched token between non-adjacent nodes with filter option', () => {
expect(
getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
filter: (t) => t.type !== 'Identifier',
})!.value,
).toEqual('=');
});

it('should retrieve first token or comment between non-adjacent nodes with includeComments option', () => {
expect(
getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { includeComments: true })!.value,
).toEqual('B');
});

it('should retrieve first token or comment between non-adjacent nodes with includeComments and skip options', () => {
expect(
getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { includeComments: true, skip: 1 })!
.value,
).toEqual('=');
});

it('should retrieve first token or comment between non-adjacent nodes with includeComments and skip and filter options', () => {
expect(
getFirstTokenBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
includeComments: true,
skip: 1,
filter: (t) => t.type !== 'Punctuator',
})!.value,
).toEqual('C');
});
});

// https://github.com/eslint/eslint/blob/v9.39.1/tests/lib/languages/js/source-code/token-store.js#L1298-L1382
describe('when calling getLastTokensBetween', () => {
/* oxlint-disable-next-line no-disabled-tests expect-expect */
it('is to be implemented');
/* oxlint-disable-next-line no-unused-expressions */
getLastTokensBetween;
it('should retrieve zero tokens between adjacent nodes', () => {
expect(getLastTokensBetween(BinaryExpression, CallExpression).map((token) => token.value)).toEqual([]);
});

it('should retrieve multiple tokens between non-adjacent nodes with count option', () => {
expect(
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, 2).map((token) => token.value),
).toEqual(['a', '*']);
expect(
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { count: 2 }).map(
(token) => token.value,
),
).toEqual(['a', '*']);
});

it('should retrieve matched tokens between non-adjacent nodes with filter option', () => {
expect(
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
filter: (t) => t.type !== 'Punctuator',
}).map((token) => token.value),
).toEqual(['a']);
});

it('should retrieve all tokens between non-adjacent nodes with empty object option', () => {
expect(
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {}).map((token) => token.value),
).toEqual(['=', 'a', '*']);
});

it('should retrieve all tokens and comments between non-adjacent nodes with includeComments option', () => {
expect(
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, { includeComments: true }).map(
(token) => token.value,
),
).toEqual(['B', '=', 'C', 'a', 'D', '*']);
});

it('should retrieve multiple tokens between non-adjacent nodes with includeComments and count options', () => {
expect(
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
includeComments: true,
count: 3,
}).map((token) => token.value),
).toEqual(['a', 'D', '*']);
});

it('should retrieve multiple tokens and comments between non-adjacent nodes with includeComments and filter options', () => {
expect(
getLastTokensBetween(VariableDeclaratorIdentifier, BinaryExpression.right, {
includeComments: true,
filter: (t) => t.type !== 'Punctuator',
}).map((token) => token.value),
).toEqual(['B', 'C', 'a', 'D']);
});
});

describe('when calling getLastTokenBetween', () => {
Expand Down
Loading