JS プラグイン
Oxlint は、独自に作成されたものや npm から入手したものも含めて、JavaScript で書かれたプラグインをサポートしています。
Oxlint のプラグイン API は ESLint v9 以降と互換性があります。そのため、既存の大多数の ESLint プラグインは、Oxlint でそのまま動作するはずです。
私たちは、すべての ESLint プラグイン API を実装することを目指しており、すぐに Oxlint で 任意 の ESLint プラグインを実行できるようになります。
WARNING
JS プラグインは現在、技術的プレビュー段階にあり、開発が継続的に進行中です。 ほぼすべての ESLint プラグイン API が実装済み(下記参照)です。
すべての API は、ESLint と同一の振る舞いをするべきです。もし振る舞いの違いを見つけた場合、それはバグです —— 報告してください。
JS プラグインの使用方法
.oxlintrc.json設定ファイルのjsPluginsセクションに、プラグインへのパスを追加します。rulesセクションに、プラグインからのルールを追加します。
パスは有効なインポート指定子であれば何でもよいです。例:./plugin.js、eslint-plugin-foo、@foo/eslint-plugin。 パスは設定ファイル自体に対して相対的に解決されます。
// .oxlintrc.json
{
"jsPlugins": ["./path/to/my-plugin.js", "eslint-plugin-whatever", "@foobar/eslint-plugin"],
"rules": {
"my-plugin/rule1": "error",
"my-plugin/rule2": "warn",
"whatever/rule1": "error",
"whatever/rule2": "warn",
"@foobar/rule1": "error"
}
// ... その他の設定 ...
}プラグインエイリアス
プラグインに別の名前(エイリアス)を定義することもできます。これは以下の状況で有用です:
- デフォルトのプラグイン名が、ネイティブな Oxlint プラグインの名前と衝突している場合(例:jsdoc、react など)。
- デフォルトのプラグイン名が非常に長い場合。
- Oxlint がネイティブにサポートしているプラグインを使用したいが、必要な特定のルールがまだ Oxlint のネイティブ版で実装されていない場合。
{
"jsPlugins": [
// `jsdoc` は予約語であり、Oxlint がネイティブにサポートしているため
{
"name": "jsdoc-js",
"specifier": "eslint-plugin-jsdoc"
},
// ネームを短縮
{
"name": "short",
"specifier": "eslint-plugin-with-name-so-very-very-long"
},
// エイリアスをつけたくないプラグインは、単なる指定子としてリストアップ
"eslint-plugin-whatever"
],
"rules": {
"jsdoc-js/check-alignment": "error",
"short/rule1": "error",
"whatever/rule2": "error"
}
}JS プラグインの作成方法
ESLint 互換の API
Oxlint は、ESLint とまったく同じプラグイン API を提供しています。以下を参照してください:
5つのクラス宣言より多いファイルを警告する簡単なプラグイン:
// plugin.js
const rule = {
create(context) {
let classCount = 0;
return {
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "クラスが多すぎます", node });
}
},
};
},
};
const plugin = {
meta: {
name: "best-plugin-ever",
},
rules: {
"max-classes": rule,
},
};
export default plugin;// .oxlintrc.json
{
"jsPlugins": ["./plugin.js"],
"rules": {
"best-plugin-ever/max-classes": "error"
}
}別の API
Oxlint は、若干異なるがよりパフォーマンスに優れた代替的な API も提供しています。
この API で作成されたルールは 完全に ESLint と互換性があります(下記参照)。
上記と同じルールを、代替 API を使って実装:
import { eslintCompatPlugin } from "@oxlint/plugins";
const rule = {
createOnce(context) {
// カウンター変数を定義
let classCount;
return {
before() {
// 各ファイルの AST 遍歴の前にカウンターをリセット
classCount = 0;
},
// 以前と同様
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "クラスが多すぎます", node });
}
},
};
},
};
const plugin = eslintCompatPlugin({
meta: {
name: "best-plugin-ever",
},
rules: {
"max-classes": rule,
},
});
export default plugin;主な違いは以下の通りです:
- プラグインオブジェクトを
eslintCompatPlugin(...)で囲む。
- const plugin = {
+ const plugin = eslintCompatPlugin({createの代わりにcreateOnceを使う。
- create(context) {
+ createOnce(context) {create(ESLint の API)は 各ファイルごとに繰り返し 呼ばれる一方、createOnceは 一度だけ 呼ばれます。 各ファイルごとの初期化処理はbeforeフック内で行います。
- let classCount = 0;
+ let classCount;
return {
+ before() {
+ classCount = 0; // カウンターをリセット
+ },
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "クラスが多すぎます", node });
}
},
};eslintCompatPlugin とは?
eslintCompatPlugin は、プラグイン内の各ルールに create メソッドを追加し、それを createOnce に委任します。
つまり、このプラグインは、Oxlint でも ESLint でも使用可能になります。
- Oxlint では、高速な
createOnceAPI によるパフォーマンス向上を得られます。 - ESLint では、オリジナルの ESLint
createAPI で書いたものと全く同じ動作になります。
NPM にプラグインを公開する場合は、@oxlint/plugins を 実行時依存関係(開発依存関係ではなく)として追加してください。
AST 遍歴のスキップ
before フックから false を返すことで、該当ファイルのルール実行をスキップできます。
// 「// @skip-me」コメントで始まるファイルには、このルールは実行されません
const rule = {
createOnce(context) {
return {
before() {
if (context.sourceCode.text.startsWith("// @skip-me")) {
return false;
}
},
FunctionDeclaration(node) {
// 処理を実行
},
};
},
};これは、ESLint における以下のパターンと同等です:
const rule = {
create(context) {
if (context.sourceCode.text.startsWith("// @skip-me")) {
return {};
}
return {
FunctionDeclaration(node) {
// 処理を実行
},
};
},
};before フック
before フックは、AST の訪問の前に実行されます。
重要:before フックは、すべてのファイルで実行されることを保証していません。
現時点では実行されますが、将来、ルールが「興味を持っている」AST ノードと、実際に含まれるノードに基づいて、ルールの実行が必要かどうかを判定するロジックを Rust 側に追加する予定です。 これにより、無駄な呼び出しを回避し、パフォーマンスを向上させることができます。
上記の例では、ファイルに FunctionDeclaration が含まれていない場合、そのファイルに対するルール実行自体が完全にスキップされ、before フックもスキップ されます。
すべてのファイルで一度だけ必ず実行されるコードが必要な場合、Program のビジターオブジェクトを実装してください:
const rule = {
createOnce(context) {
return {
Program(node) {
// どのファイルにも `FunctionDeclaration` がなくても、この処理は常に実行されます
},
FunctionDeclaration(node) {
/* 処理を実行 */
},
};
},
};after フック
after フックも存在します。これは、ファイルごとに 1回だけ 実行され、全体の AST 遍歴(Program:exit 以降)の後に行われます。
ルールの AST 遍歴中に使用した高コストなリソースのクリーンアップに使用します。
before フックが false を返してファイルのルール実行をスキップした場合、after フックもスキップされます。
before フックと同様、after フックもすべてのファイルで実行されるわけではありません(上記参照)。
なぜ代替 API は高速なのか?
簡単に言うと、現時点ではそうではありません。しかし、すぐにもなります。
初回の技術的プレビュー公開の前段階で、我々は長期間にわたる「研究開発(R&D)」プロセスを経ました。多くの最適化の機会を特定し、次世代の Oxlint プラグインの原型を構築しました。そのパフォーマンスは、極めて良好です。
これらの最適化の多くは今のリリースには含まれていませんが、今後数ヶ月で仕上げ、逐次 Oxlint に統合していきます。
代替 API は、これらの最適化を活用するように設計されています。この代替 API で開発を開始しておくことで、将来的に oxlint のバージョンをアップデートするだけで、コードの変更なしに劇的な速度向上が得られます。
これらの最適化とは何か?
上記の「クラスは5つまで」ルールの例に戻ります:
const rule = {
create(context) {
let classCount = 0;
return {
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "クラスが多すぎます", node });
}
},
};
},
};create メソッドは、各ファイルごとに1回ずつ呼び出され、毎回新しい context オブジェクトが渡されます。
なぜこれが問題なのか?
最大限のパフォーマンスを実現するには、ルールが「関心を持つ」べきである静的な情報(例えば、どの種類のノードに関心があるか)を事前に知っている必要があります。その情報をもとに、次の2つの最適化が可能です:
- 通常、JS 側でアストを再帰的に走査する必要がない。代わりに、Rust 側でのアスト走査時に、関連するアストノードへの「ポインタ」リストを生成し、それを送信する。これにより、JS 側は全アストを探索するのではなく、直接関連ノードに「ジャンプ」できる。
- ファイル内にルールが関心を持つアストノード(この例ではクラス宣言)が まったく存在しない 場合は、そのファイルについてはそもそも JS 側への呼び出しをスキップできる。
しかし、JS は動的言語であり、create は 何でも 行える可能性があります。各呼び出しで完全に異なるビジターオブジェクトを返すことも可能です。そのため、create を呼んで初めて、「本当に create を呼び出す必要があるのか?」を確認しなければならないのです。
それに対し、代替 API では createOnce は一度だけ呼び出されるため、ルールの内容が確定します。これにより上記の最適化が可能になります。
明確に言うと、create API が、ESLint の設計上のミスだったわけではありません。ただし、一旦 Rust-JS の相互作用が考慮に入ると、いくつかの課題が生じます。
API 対応状況
Oxlint は、ほぼすべての ESLint API を対応しています:
- アストの走査。
- アストの探索(
node.parent、context.sourceCode.getAncestors)。 - フィックス機能。
- ルールオプション。
- セレクタ(ESLint ドキュメント)。
SourceCodeAPI(例:context.sourceCode.getText(node))。SourceCodeトークン関連の API(例:context.sourceCode.getTokens(node))。- スコープ解析。
- コントロールフロー解析(コードパス)。
- インライン無効化ディレクティブ(
// oxlint-disable)。 - ランタイムサーバー(IDE)サポート+提案(編集中診断とクイック修復)。
まだ対応していないもの:
- カスタムファイル形式およびパーサ(例:Svelte、Vue、Angular)。
- TypeScript の型情報に依存するルール。
ESLint v9 やそれ以前に削除された ESLint API は、大多数の場合実装されません。もし、メンテナンスが停止していてかつ ESLint v9 用にアップデートされていない ESLint プラグインを使用したい場合は、自身でプラグインを修正するか、代替品を探す必要があるかもしれません。
今後数ヶ月で残りの機能を実装し、100% の ESLint プラグイン API サーフェスをサポートする予定です。
