Skip to content

Commit 0c1f82b

Browse files
committed
feat(linter/plugins): add tokens property to Program (#16020)
Add a `tokens` property to `Program`. Like ESLint, `program.tokens` contains only syntax tokens, and does not include comments. Implemented as a getter, which lazily tokenizes source text on first access, using the same mechanism as other tokens APIs. Note: If user has an AST to access the `tokens` getter on, `sourceText` must have been initialized, so the getter doesn't need to check that.
1 parent b8bf404 commit 0c1f82b

File tree

10 files changed

+113
-6
lines changed

10 files changed

+113
-6
lines changed

apps/oxlint/src-js/generated/deserialize.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Auto-generated code, DO NOT EDIT DIRECTLY!
22
// To edit this generated file you have to edit `tasks/ast_tools/src/generators/raw_transfer.rs`.
33

4+
import { tokens, initTokens } from '../plugins/tokens.js';
5+
46
let uint8,
57
uint32,
68
float64,
@@ -62,6 +64,10 @@ function deserializeProgram(pos) {
6264
Object.defineProperty(this, 'comments', { value: comments });
6365
return comments;
6466
},
67+
get tokens() {
68+
tokens === null && initTokens();
69+
return tokens;
70+
},
6571
start: 0,
6672
end,
6773
range: [0, end],

apps/oxlint/src-js/generated/types.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
// To edit this generated file you have to edit `tasks/ast_tools/src/generators/typescript.rs`.
33

44
import { Span } from '../plugins/location.ts';
5+
import { Token } from '../plugins/tokens.ts';
56
import { Comment } from '../plugins/types.ts';
6-
export { Span, Comment };
7+
export { Span, Comment, Token };
78

89
export interface Program extends Span {
910
type: 'Program';
1011
body: Array<Directive | Statement>;
1112
sourceType: ModuleKind;
1213
hashbang: Hashbang | null;
1314
comments: Comment[];
15+
tokens: Token[];
1416
parent: null;
1517
}
1618

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export interface TemplateToken extends BaseToken {
135135

136136
// Tokens for the current file parsed by TS-ESLint.
137137
// Created lazily only when needed.
138-
let tokens: Token[] | null = null;
138+
export let tokens: Token[] | null = null;
139139
let comments: CommentToken[] | null = null;
140140
let tokensWithComments: Token[] | null = null;
141141

@@ -146,8 +146,10 @@ let tsEslintParse: typeof import('@typescript-eslint/typescript-estree').parse |
146146

147147
/**
148148
* Initialize TS-ESLint tokens for current file.
149+
*
150+
* Caller must ensure `sourceText` is initialized before calling this function.
149151
*/
150-
function initTokens() {
152+
export function initTokens() {
151153
debugAssertIsNonNull(sourceText);
152154

153155
// Lazy-load TS-ESLint.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"jsPlugins": ["./plugin.ts"],
3+
"categories": {
4+
"correctness": "off"
5+
},
6+
"rules": {
7+
"tokens-plugin/tokens": "error"
8+
}
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Leading comment
2+
3+
let x = /* inline comment */ 1;
4+
5+
// Another comment
6+
let y = 2;
7+
8+
// Trailing comment
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Exit code
2+
1
3+
4+
# stdout
5+
```
6+
x tokens-plugin(tokens): Tokens:
7+
| Keyword loc=3:0-3:3 range=20-23 "let"
8+
| Identifier loc=3:4-3:5 range=24-25 "x"
9+
| Punctuator loc=3:6-3:7 range=26-27 "="
10+
| Numeric loc=3:29-3:30 range=49-50 "1"
11+
| Punctuator loc=3:30-3:31 range=50-51 ";"
12+
| Keyword loc=6:0-6:3 range=72-75 "let"
13+
| Identifier loc=6:4-6:5 range=76-77 "y"
14+
| Punctuator loc=6:6-6:7 range=78-79 "="
15+
| Numeric loc=6:8-6:9 range=80-81 "2"
16+
| Punctuator loc=6:9-6:10 range=81-82 ";"
17+
,-[files/index.js:1:1]
18+
1 | ,-> // Leading comment
19+
2 | |
20+
3 | | let x = /* inline comment */ 1;
21+
4 | |
22+
5 | | // Another comment
23+
6 | | let y = 2;
24+
7 | |
25+
8 | `-> // Trailing comment
26+
`----
27+
28+
Found 0 warnings and 1 error.
29+
Finished in Xms on 1 file using X threads.
30+
```
31+
32+
# stderr
33+
```
34+
WARNING: JS plugins are experimental and not subject to semver.
35+
Breaking changes are possible while JS plugins support is under development.
36+
```
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Plugin, Rule } from '../../../dist/index.js';
2+
3+
const rule: Rule = {
4+
create(context) {
5+
const { sourceCode } = context,
6+
{ ast } = sourceCode;
7+
8+
// Note: Comments should not appear in `ast.tokens`
9+
context.report({
10+
message:
11+
`Tokens:\n` +
12+
ast.tokens
13+
.map(
14+
({ type, loc, range, value }) =>
15+
`${type.padEnd(17)} ` +
16+
`loc=${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column} `.padEnd(16) +
17+
`range=${range[0]}-${range[1]} `.padEnd(10) +
18+
`"${value}"`,
19+
)
20+
.join('\n'),
21+
node: { range: [0, sourceCode.text.length] },
22+
});
23+
24+
return {};
25+
},
26+
};
27+
28+
const plugin: Plugin = {
29+
meta: { name: 'tokens-plugin' },
30+
rules: { tokens: rule },
31+
};
32+
33+
export default plugin;

crates/oxc_ast/src/serialize/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ impl Program<'_> {
142142
return comments;
143143
},
144144
/* END_IF */
145+
/* IF LINTER */
146+
get tokens() {
147+
if (tokens === null) initTokens();
148+
return tokens;
149+
},
150+
/* END_IF */
145151
start,
146152
end,
147153
...(RANGE && { range: [start, end] }),

tasks/ast_tools/src/generators/raw_transfer.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ fn generate_deserializers(
134134

135135
#[rustfmt::skip]
136136
let mut code = format!("
137+
/* IF LINTER */
138+
import {{ tokens, initTokens }} from '../plugins/tokens.js';
139+
/* END_IF */
140+
137141
let uint8, uint32, float64, sourceText, sourceIsAscii, sourceByteLen;
138142
139143
let astId = 0;

tasks/ast_tools/src/generators/typescript.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,11 +479,11 @@ fn amend_oxlint_types(code: &str) -> String {
479479

480480
let mut code = SPAN_REGEX.replace(code, SpanReplacer).into_owned();
481481

482-
// Add `comments` field to `Program`
482+
// Add `comments` and `tokens` fields to `Program`
483483
#[expect(clippy::items_after_statements)]
484484
const HASHBANG_FIELD: &str = "hashbang: Hashbang | null;";
485485
let index = code.find(HASHBANG_FIELD).unwrap();
486-
code.insert_str(index + HASHBANG_FIELD.len(), "comments: Comment[];");
486+
code.insert_str(index + HASHBANG_FIELD.len(), "comments: Comment[]; tokens: Token[];");
487487

488488
// Make `parent` fields non-optional
489489
#[expect(clippy::disallowed_methods)]
@@ -492,8 +492,9 @@ fn amend_oxlint_types(code: &str) -> String {
492492
#[rustfmt::skip]
493493
code.insert_str(0, "
494494
import { Span } from '../plugins/location.ts';
495+
import { Token } from '../plugins/tokens.ts';
495496
import { Comment } from '../plugins/types.ts';
496-
export { Span, Comment };
497+
export { Span, Comment, Token };
497498
498499
");
499500

0 commit comments

Comments
 (0)