Skip to content

Commit 1c6b6b2

Browse files
committed
docs(linter): add JS plugins documentation
1 parent cc0f9ab commit 1c6b6b2

File tree

3 files changed

+363
-4
lines changed

3 files changed

+363
-4
lines changed

.vitepress/config/en.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export const enConfig = defineLocaleConfig("root", {
6464
text: "Plugins",
6565
link: "/docs/guide/usage/linter/plugins",
6666
},
67+
{
68+
text: "JS Plugins",
69+
link: "/docs/guide/usage/linter/js-plugins",
70+
},
6771
{
6872
text: "Automatic Fixes",
6973
link: "/docs/guide/usage/linter/automatic-fixes",
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# JS Plugins
6+
7+
Oxlint supports plugins written in JS - either custom-written, or from NPM.
8+
9+
Oxlint's plugin API is compatible with ESLint, so many existing ESLint plugins should work out of the box with Oxlint.
10+
11+
:::warning
12+
JS plugins are currently in technical preview, and remain under heavy development.
13+
Not all of ESLint's plugin API is implemented yet (see [here](#api-support)).
14+
15+
All APIs which _are_ implemented should behave identically to ESLint. If you find any differences in behavior,
16+
that's a bug - please [report it](https://github.com/oxc-project/oxc/issues/new?template=linter_bug_report.yaml).
17+
:::
18+
19+
We are working towards implementing _all_ of ESLint's plugin APIs, and Oxlint will eventually be able to run
20+
_any_ ESLint plugin.
21+
22+
## Using JS plugins
23+
24+
1. Add path to the plugin to `.oxlintrc.json` config file, under `jsPlugins`.
25+
2. Add rules from the plugin, under `rules`.
26+
27+
Path can be any valid import specifier e.g. `./plugin.js` or `plugin-package`.
28+
Paths are resolved relative to the config file itself.
29+
30+
```json
31+
// .oxlintrc.json
32+
{
33+
"jsPlugins": [
34+
"./path/to/my-plugin.js",
35+
"eslint-plugin-whatever"
36+
],
37+
"rules": {
38+
"my-plugin/rule1": "error",
39+
"my-plugin/rule2": "warn",
40+
"whatever/rule1": "error",
41+
"whatever/rule2": "warn"
42+
}
43+
// ... other config ...
44+
}
45+
```
46+
47+
## Writing JS plugins
48+
49+
### ESLint-compatible API
50+
51+
Oxlint provides a plugin API identical to ESLint's. See ESLint's docs on
52+
[creating a plugin](https://eslint.org/docs/latest/extend/plugins) and
53+
[custom rules](https://eslint.org/docs/latest/extend/custom-rules).
54+
55+
A simple plugin which flags files containing more than 5 class declarations:
56+
57+
```js
58+
// plugin.js
59+
const rule = {
60+
create(context) {
61+
let classCount = 0;
62+
63+
return {
64+
ClassDeclaration(node) {
65+
classCount++;
66+
if (classCount === 6) {
67+
context.report({ message: "Too many classes", node });
68+
}
69+
},
70+
};
71+
},
72+
};
73+
74+
const plugin = {
75+
meta: {
76+
name: "best-plugin-ever",
77+
},
78+
rules: {
79+
"max-classes": rule,
80+
},
81+
};
82+
83+
export default plugin;
84+
```
85+
86+
```json
87+
// .oxlintrc.json
88+
{
89+
"jsPlugins": ["./plugin.js"],
90+
"rules": {
91+
"best-plugin-ever/max-classes": "error"
92+
}
93+
}
94+
```
95+
96+
### Alternative API
97+
98+
Oxlint also provides a slightly different alternative API which is more performant.
99+
100+
Rules created with this API **remain compatible with ESLint** (see [below](#what-does-definerule-do)).
101+
102+
Same rule as above, using the alternative API:
103+
104+
```js
105+
import { defineRule } from "oxlint";
106+
107+
const rule = defineRule({
108+
createOnce(context) {
109+
// Define counter variable
110+
let classCount;
111+
112+
return {
113+
before() {
114+
// Reset counter before traversing AST of each file
115+
classCount = 0;
116+
},
117+
// Same as before
118+
ClassDeclaration(node) {
119+
classCount++;
120+
if (classCount === 6) {
121+
context.report({ message: "Too many classes", node });
122+
}
123+
},
124+
};
125+
},
126+
});
127+
```
128+
129+
The differences are:
130+
131+
1. Wrap the rule object in `defineRule(...)`.
132+
133+
```diff
134+
- const rule = {
135+
+ const rule = defineRule({
136+
```
137+
138+
2. Use `createOnce` instead of `create`.
139+
140+
```diff
141+
- create(context) {
142+
+ createOnce(context) {
143+
```
144+
145+
3. `create` (ESLint's API) is called repeatedly _for each file_, whereas `createOnce` is called once only.
146+
Perform any per-file setup in `before` hook instead.
147+
148+
```diff
149+
- let classCount = 0;
150+
+ let classCount;
151+
152+
return {
153+
+ before() {
154+
+ classCount = 0; // Reset counter
155+
+ },
156+
ClassDeclaration(node) {
157+
classCount++;
158+
if (classCount === 6) {
159+
context.report({ message: "Too many classes", node });
160+
}
161+
},
162+
};
163+
},
164+
});
165+
```
166+
167+
#### What does `defineRule` do?
168+
169+
`defineRule` adds a `create` method to the rule, which delegates to `createOnce`.
170+
171+
**This means the rule can be used with either Oxlint or ESLint.**
172+
173+
- In Oxlint, it'll get a perf boost from the faster `createOnce` API.
174+
- In ESLint, it'll work exactly the same as if it was written with the original ESLint `create` API.
175+
176+
#### `definePlugin`
177+
178+
If your plugin includes multiple rules, wrapping the whole plugin in `definePlugin` has same effect as wrapping each
179+
individual rule in `defineRule`.
180+
181+
```js
182+
import { definePlugin } from "oxlint";
183+
184+
const plugin = definePlugin({
185+
meta: { name: "my-plugin" },
186+
rules: {
187+
"no-foo": rule1,
188+
"no-bar": rule2,
189+
},
190+
});
191+
```
192+
193+
#### Skipping AST traversal
194+
195+
Returning `false` from `before` hook causes the rule to skip this file.
196+
197+
```js
198+
// This rule does not run on files which start with a `// @skip-me` comment
199+
const rule = defineRule({
200+
createOnce(context) {
201+
return {
202+
before() {
203+
if (context.sourceCode.text.startsWith("// @skip-me")) {
204+
return false;
205+
}
206+
},
207+
FunctionDeclaration(node) {
208+
// Do stuff
209+
},
210+
};
211+
},
212+
});
213+
```
214+
215+
This is equivalent to this pattern in ESLint:
216+
217+
```js
218+
const rule = {
219+
create(context) {
220+
if (context.sourceCode.text.startsWith("// @skip-me")) {
221+
return {};
222+
}
223+
224+
return {
225+
FunctionDeclaration(node) {
226+
// Do stuff
227+
},
228+
};
229+
},
230+
};
231+
```
232+
233+
#### `before` hook
234+
235+
`before` hook runs before the AST is visited.
236+
237+
IMPORTANT: `before` hook is NOT guaranteed to run on every file.
238+
239+
At present it does, but in future we intend to add logic on Rust side to determine if the rule needs to run or not,
240+
based on what AST nodes the rule is "interested in", and what the AST contains.
241+
This will enable better performance by skipping redundant calls from Rust into JS.
242+
243+
In example above, if a file does not contain any `FunctionDeclaration`s, running the rule on that file will be skipped
244+
entirely, _including_ skipping the `before` hook.
245+
246+
If you need code to always run once for every file, implement a `Program` visitor instead:
247+
248+
```js
249+
const rule = defineRule({
250+
createOnce(context) {
251+
return {
252+
Program(node) {
253+
// This always runs for every file, even if
254+
// it doesn't contain any `FunctionDeclaration`s
255+
},
256+
FunctionDeclaration(node) {/* do stuff */},
257+
};
258+
},
259+
});
260+
```
261+
262+
#### `after` hook
263+
264+
There is also an `after` hook. It runs once per file, _after_ the whole AST has been traversed (after `Program:exit`).
265+
266+
Use it to clean up any expensive resources used during the rule's AST traversal.
267+
268+
If `before` hook returns `false` to skip running the rule on the file, `after` hook will be skipped too.
269+
270+
Same as `before` hook, `after` hook is NOT guaranteed to run on every file (see [above](#when-before-hook-runs)).
271+
272+
### Why is the alternative API faster?
273+
274+
Short answer: Right now it isn't. But it _will be soon_.
275+
276+
Prior to the initial technical preview release of JS plugins, we have undergone a lengthy "R&D" process. We have
277+
identified many optimization opportunities, and have prototyped the _next_ version of Oxlint plugins, which has
278+
_extremely_ good performance.
279+
280+
Many of those optimizations are not in the current release, but we'll be polishing them and folding them into Oxlint
281+
over the next few months.
282+
283+
The alternative API is designed to enable and capitalize on these optimizations. By adopting the alternative API now,
284+
plugin authors will see their plugins get a significant speed boost in future "for free", just by bumping `oxlint`
285+
version, without any code changes.
286+
287+
#### What are those optimizations?
288+
289+
Returning to the "no more than 5 classes" rule example from above:
290+
291+
```js
292+
const rule = {
293+
create(context) {
294+
let classCount = 0;
295+
296+
return {
297+
ClassDeclaration(node) {
298+
classCount++;
299+
if (classCount === 6) {
300+
context.report({ message: "Too many classes", node });
301+
}
302+
},
303+
};
304+
},
305+
};
306+
```
307+
308+
The `create` method is called once per file, each time with a new `context` object.
309+
310+
Why is that a problem?
311+
312+
For maximum performance, ideally we want to statically know what AST nodes the rule is
313+
"interested in". With that information, we can perform 2 optimizations:
314+
315+
1. Don't walk the AST on JS side. Instead, during traversal of AST on Rust side, compile a list of "pointers" to
316+
the relevant AST nodes. Send that list to JS, and JS can "jump" straight to the relevant AST nodes, rather than
317+
searching the whole AST.
318+
319+
2. If the AST doesn't contain _any_ AST nodes which match what the rule is interested in (in example above, if file
320+
contains no class declarations), skip calling into JS entirely for that file.
321+
322+
But JS is a dynamic language, and `create` could do _anything_. It could return a completely different visitor each time
323+
it's called. So we have to call `create` to find out whether we needed to call `create`!
324+
325+
In comparison, with the alternative API, `createOnce` is called only once, and we then know what the rule does.
326+
This enables the above optimizations.
327+
328+
To be clear, the `create` API was _not_ a poor design decision on ESLint's part. It just presents some difficulties once
329+
Rust-JS interop comes into play.
330+
331+
## API support
332+
333+
Oxlint supports most of the APIs typically used in plugins/rules which rely on AST inspection.
334+
That includes most "fix code"-type rules.
335+
336+
It does not yet support token-based APIs, so stylistic (formatting) rules will not work yet.
337+
338+
Supported:
339+
340+
- AST traversal.
341+
- AST exploration (`node.parent`, `context.sourceCode.getAncestors`).
342+
- Fixes.
343+
- Selectors ([ESLint docs](https://eslint.org/docs/latest/extend/selectors)).
344+
- `SourceCode` APIs (e.g. `context.sourceCode.getText(node)`).
345+
- Plugins written in TypeScript (with NodeJS 22.18.0+).
346+
347+
Not supported yet:
348+
349+
- Language server (IDE) support.
350+
- Rule options.
351+
- Suggestions.
352+
- Scope analysis.
353+
- `SourceCode` APIs related to tokens (e.g. `context.sourceCode.getTokens(node)`).
354+
- `SourceCode` APIs related to comments (e.g. `context.sourceCode.getAllComments()`).
355+
- Control flow analysis.
356+
357+
We will be filling in the gaps in API support over the next few months, aiming to eventually support 100% of ESLint's
358+
plugin API surface.

src/docs/guide/usage/linter/plugins.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ outline: [2, 3]
77
Oxlint supports several of the most popular ESLint plugins out of the box with
88
most rules in `recommended` configs already implemented.
99

10-
:::warning
11-
Oxlint does not support custom plugins at this time. We have plans to implement
12-
this in the future. You can track its status on [our backlog](https://github.com/oxc-project/oxc/issues/9905) and provide feedback in [this discussion](https://github.com/oxc-project/oxc/discussions/10342).
13-
:::
10+
Oxlint also supports [plugins written in JS](/docs/guide/usage/linter/js-plugins.html).
1411

1512
## Enabling Plugins
1613

0 commit comments

Comments
 (0)