Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e98aca7
implement `sourceCode.getTokens`
lilnasy Nov 18, 2025
2f8f2b4
format linted file and test snapshot
lilnasy Nov 20, 2025
0b26391
make `typescript` a full dependency for now
lilnasy Nov 20, 2025
ca9492b
add `regex` field to `RegularExpressionToken`
lilnasy Nov 20, 2025
1243eb3
add todo for enabling jsx when needed
lilnasy Nov 20, 2025
3833be3
add testing todo
lilnasy Nov 20, 2025
50f3589
`tsTokens` -> `tokens`
lilnasy Nov 20, 2025
625172d
add todo for `countOptions` and `afterCount` for `getTokens()`
lilnasy Nov 20, 2025
fdb8449
move token types to `tokens.ts`
lilnasy Nov 20, 2025
c3d5274
refactor test
lilnasy Nov 20, 2025
7deb6ab
add slash lexing corner case
lilnasy Nov 20, 2025
c8e5020
implement `getTokens()`
lilnasy Nov 20, 2025
0a6ea49
test `includeComments: true`
lilnasy Nov 20, 2025
2e60bcc
vendor `eslint` tests
lilnasy Nov 20, 2025
c9f1c30
skip tests failing due to incompatibility
lilnasy Nov 20, 2025
ab852e0
handle `count === 0` edge case
lilnasy Nov 21, 2025
279e241
style
lilnasy Nov 21, 2025
54fd74e
reset `comments` and `tokensWithComments` arrays
lilnasy Nov 21, 2025
7779c02
move `assertIsNonNull` to the beginning of the function
lilnasy Nov 21, 2025
da786dd
Update output.snap.md
lilnasy Nov 21, 2025
84d90e3
ignore `afterCount` if there is no `beforeCount`
lilnasy Nov 21, 2025
f75f389
unskip tests
lilnasy Nov 21, 2025
344d685
fix lint errors by inlining `check()`
lilnasy Nov 21, 2025
58396ca
update test case
lilnasy Nov 21, 2025
24e2c08
bundle `typescript`
lilnasy Nov 21, 2025
56c73d9
remove `typescript` dependency from published `package.json`
lilnasy Nov 21, 2025
d115185
handle excessive `beforeCount`
lilnasy Nov 21, 2025
25de492
add a sanity check test case
lilnasy Nov 21, 2025
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
7 changes: 5 additions & 2 deletions apps/oxlint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"dependencies": {
},
"devDependencies": {
"@typescript-eslint/scope-manager": "8.46.2",
"@types/esquery": "^1.5.4",
"@types/estree": "^1.0.8",
"@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/typescript-estree": "^8.47.0",
"eslint": "^9.36.0",
"esquery": "^1.6.0",
"execa": "^9.6.0",
"jiti": "^2.6.0",
"tsdown": "catalog:",
"type-fest": "^5.2.0",
"typescript": "catalog:",
"typescript": "5.9.3",
"vitest": "catalog:"
},
"napi": {
Expand Down
34 changes: 23 additions & 11 deletions apps/oxlint/src-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,28 @@ export type {
} from './plugins/scope.ts';
export type { Settings } from './plugins/settings.ts';
export type { SourceCode } from './plugins/source_code.ts';
export type { CountOptions, FilterFn, RangeOptions, SkipOptions } from './plugins/tokens.ts';
export type {
CountOptions,
FilterFn,
RangeOptions,
SkipOptions,
Token,
BooleanToken,
CommentToken,
BlockCommentToken,
LineCommentToken,
IdentifierToken,
JSXIdentifierToken,
JSXTextToken,
KeywordToken,
NullToken,
NumericToken,
PrivateIdentifierToken,
PunctuatorToken,
RegularExpressionToken,
StringToken,
TemplateToken,
} from './plugins/tokens.ts';
export type {
RuleMeta,
RuleDocs,
Expand All @@ -33,16 +54,7 @@ export type {
RuleReplacedByExternalSpecifier,
} from './plugins/rule_meta.ts';
export type { LineColumn, Location, Range, Ranged, Span } from './plugins/location.ts';
export type {
AfterHook,
BeforeHook,
Comment,
Node,
NodeOrToken,
Token,
Visitor,
VisitorWithHooks,
} from './plugins/types.ts';
export type { AfterHook, BeforeHook, Comment, Node, NodeOrToken, Visitor, VisitorWithHooks } from './plugins/types.ts';

const {
defineProperty,
Expand Down
2 changes: 2 additions & 0 deletions apps/oxlint/src-js/plugins/source_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from './location.js';
import { resetScopeManager, SCOPE_MANAGER } from './scope.js';
import * as scopeMethods from './scope.js';
import { resetTokens } from './tokens.js';
import * as tokenMethods from './tokens.js';
import { assertIsNonNull } from './utils.js';

Expand Down Expand Up @@ -98,6 +99,7 @@ export function resetSourceAndAst(): void {
resetBuffer();
resetLines();
resetScopeManager();
resetTokens();
}

// `SourceCode` object.
Expand Down
215 changes: 212 additions & 3 deletions apps/oxlint/src-js/plugins/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/*
* `SourceCode` methods related to tokens.
*/

import { parse } from '@typescript-eslint/typescript-estree';
import { sourceText, initSourceText } from './source_code.js';
import { assertIsNonNull } from './utils.js';

import type { Comment, Node, NodeOrToken, Token } from './types.ts';
import type { Comment, Node, NodeOrToken } from './types.ts';
import type { Span } from './location.ts';

/**
* Options for various `SourceCode` methods e.g. `getFirstToken`.
Expand Down Expand Up @@ -44,6 +45,118 @@ export interface RangeOptions {
*/
export type FilterFn = (token: Token) => boolean;

// AST token type.
export type Token =
| BooleanToken
| CommentToken
| IdentifierToken
| JSXIdentifierToken
| JSXTextToken
| KeywordToken
| NullToken
| NumericToken
| PrivateIdentifierToken
| PunctuatorToken
| RegularExpressionToken
| StringToken
| TemplateToken;

interface BaseToken extends Omit<Span, 'start' | 'end'> {
type: Token['type'];
value: string;
}

export interface BooleanToken extends BaseToken {
type: 'Boolean';
}

export type CommentToken = BlockCommentToken | LineCommentToken;

export interface BlockCommentToken extends BaseToken {
type: 'Block';
}

export interface LineCommentToken extends BaseToken {
type: 'Line';
}

export interface IdentifierToken extends BaseToken {
type: 'Identifier';
}

export interface JSXIdentifierToken extends BaseToken {
type: 'JSXIdentifier';
}

export interface JSXTextToken extends BaseToken {
type: 'JSXText';
}

export interface KeywordToken extends BaseToken {
type: 'Keyword';
}

export interface NullToken extends BaseToken {
type: 'Null';
}

export interface NumericToken extends BaseToken {
type: 'Numeric';
}

export interface PrivateIdentifierToken extends BaseToken {
type: 'PrivateIdentifier';
}

export interface PunctuatorToken extends BaseToken {
type: 'Punctuator';
}

export interface RegularExpressionToken extends BaseToken {
type: 'RegularExpression';
regex: {
flags: string;
pattern: string;
};
}

export interface StringToken extends BaseToken {
type: 'String';
}

export interface TemplateToken extends BaseToken {
type: 'Template';
}

// Tokens for the current file parsed by TS-ESLint.
// Created lazily only when needed.
let tokens: Token[] | null = null;
let comments: CommentToken[] | null = null;
let tokensWithComments: Token[] | null = null;

/**
* Initialize TS-ESLint tokens for current file.
*/
function initTokens() {
assertIsNonNull(sourceText);
({ tokens, comments } = parse(sourceText, {
sourceType: 'module',
tokens: true,
comment: true,
// TODO: enable jsx only when needed.
jsx: true,
}));
}

/**
* Discard TS-ESLint tokens to free memory.
*/
export function resetTokens() {
tokens = null;
comments = null;
tokensWithComments = null;
}

/**
* Get all tokens that are related to the given node.
* @param node - The AST node.
Expand All @@ -63,7 +176,103 @@ export function getTokens(
countOptions?: CountOptions | number | FilterFn | null,
afterCount?: number | null,
): Token[] {
throw new Error('`sourceCode.getTokens` not implemented yet'); // TODO
if (tokens === null) initTokens();

assertIsNonNull(tokens);
assertIsNonNull(comments);

/**
* Maximum number of tokens to return.
*/
const count = typeof countOptions === 'object' && countOptions !== null ? countOptions.count : null;

/**
* Number of preceding tokens to additionally return.
*/
const beforeCount = typeof countOptions === 'number' ? countOptions : 0;

/**
* Number of following tokens to additionally return.
*/
afterCount =
(typeof countOptions === 'number' || typeof countOptions === 'undefined') && typeof afterCount === 'number'
? afterCount
: 0;

/**
* 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;
}

let sliceStart = nodeTokens.length;
let sliceEnd: number | undefined = undefined;

const { range } = node,
rangeStart = range[0],
rangeEnd = range[1];

// Binary search for first token within `node`'s range.
for (let lo = 0, hi = nodeTokens.length; lo < hi; ) {
const mid = (lo + hi) >> 1;
if (nodeTokens[mid].range[0] < rangeStart) {
lo = mid + 1;
} else {
sliceStart = hi = mid;
}
}

// Binary search for the first token outside `node`'s range.
for (let lo = sliceStart, hi = nodeTokens.length; lo < hi; ) {
const mid = (lo + hi) >> 1;
if (nodeTokens[mid].range[0] < rangeEnd) {
lo = mid + 1;
} else {
sliceEnd = hi = mid;
}
}

sliceStart = Math.max(0, sliceStart - beforeCount);
// `sliceEnd` would remain undefined here if the node contains the last token of the file.
if (sliceEnd !== undefined) sliceEnd += afterCount;

nodeTokens = nodeTokens.slice(sliceStart, sliceEnd);

// Logically, filter must remain before count.
if (filter) {
nodeTokens = nodeTokens.filter(filter);
}
if (typeof count === 'number' && count < nodeTokens.length) {
nodeTokens = nodeTokens.slice(0, count);
}

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

Expand Down
7 changes: 1 addition & 6 deletions apps/oxlint/src-js/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface Visitor {
*/

import type { Span } from './location.ts';
import type { Token } from './tokens.ts';

import type { VisitorObject as Visitor } from '../generated/visitor.d.ts';
export type { Visitor };
Expand All @@ -30,12 +31,6 @@ export type VisitFn = (node: Node) => void;
// AST node type.
export interface Node extends Span {}

// AST token type.
export interface Token extends Span {
type: string;
value: string;
}

// Currently we only support `Node`s, but will add support for `Token`s later.
export type NodeOrToken = Node | Token;

Expand Down
4 changes: 4 additions & 0 deletions apps/oxlint/test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ describe('oxlint CLI', () => {
await testFixture('sourceCode_scope_methods');
});

it('should support token helper methods in `context.sourceCode`', async () => {
await testFixture('sourceCode_token_methods');
});

it('should support languageOptions', async () => {
await testFixture('languageOptions');
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"jsPlugins": ["./plugin.ts"],
"categories": {
"correctness": "off"
},
"rules": {
"token-plugin/token": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/*A*/ var answer /*B*/ = /*C*/ a /*D*/ * b; /*E*/ //F
call();
/*Z*/
Loading
Loading