Skip to content

JS プラグイン

Oxlint は、独自に作成されたものや npm から入手したものも含めて、JavaScript で書かれたプラグインをサポートしています。

Oxlint のプラグイン API は ESLint v9 以降と互換性があります。そのため、既存の大多数の ESLint プラグインは、Oxlint でそのまま動作するはずです。

私たちは、すべての ESLint プラグイン API を実装することを目指しており、すぐに Oxlint で 任意 の ESLint プラグインを実行できるようになります。

WARNING

JS プラグインは現在、技術的プレビュー段階にあり、開発が継続的に進行中です。 ほぼすべての ESLint プラグイン API が実装済み(下記参照)です。

すべての API は、ESLint と同一の振る舞いをするべきです。もし振る舞いの違いを見つけた場合、それはバグです —— 報告してください

JS プラグインの使用方法

  1. .oxlintrc.json 設定ファイルの jsPlugins セクションに、プラグインへのパスを追加します。
  2. rules セクションに、プラグインからのルールを追加します。

パスは有効なインポート指定子であれば何でもよいです。例:./plugin.jseslint-plugin-foo@foo/eslint-plugin。 パスは設定ファイル自体に対して相対的に解決されます。

json
// .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 のネイティブ版で実装されていない場合。
json
{
  "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つのクラス宣言より多いファイルを警告する簡単なプラグイン:

js
// 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;
json
// .oxlintrc.json
{
  "jsPlugins": ["./plugin.js"],
  "rules": {
    "best-plugin-ever/max-classes": "error"
  }
}

別の API

Oxlint は、若干異なるがよりパフォーマンスに優れた代替的な API も提供しています。

この API で作成されたルールは 完全に ESLint と互換性があります下記参照)。

上記と同じルールを、代替 API を使って実装:

js
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;

主な違いは以下の通りです:

  1. プラグインオブジェクトを eslintCompatPlugin(...) で囲む。
diff
- const plugin = {
+ const plugin = eslintCompatPlugin({
  1. create の代わりに createOnce を使う。
diff
-   create(context) {
+   createOnce(context) {
  1. create(ESLint の API)は 各ファイルごとに繰り返し 呼ばれる一方、createOnce一度だけ 呼ばれます。 各ファイルごとの初期化処理は before フック内で行います。
diff
-     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 では、高速な createOnce API によるパフォーマンス向上を得られます。
  • ESLint では、オリジナルの ESLint create API で書いたものと全く同じ動作になります。

NPM にプラグインを公開する場合は、@oxlint/plugins実行時依存関係(開発依存関係ではなく)として追加してください。

AST 遍歴のスキップ

before フックから false を返すことで、該当ファイルのルール実行をスキップできます。

js
// 「// @skip-me」コメントで始まるファイルには、このルールは実行されません
const rule = {
  createOnce(context) {
    return {
      before() {
        if (context.sourceCode.text.startsWith("// @skip-me")) {
          return false;
        }
      },
      FunctionDeclaration(node) {
        // 処理を実行
      },
    };
  },
};

これは、ESLint における以下のパターンと同等です:

js
const rule = {
  create(context) {
    if (context.sourceCode.text.startsWith("// @skip-me")) {
      return {};
    }

    return {
      FunctionDeclaration(node) {
        // 処理を実行
      },
    };
  },
};

before フック

before フックは、AST の訪問の前に実行されます。

重要:before フックは、すべてのファイルで実行されることを保証していません。

現時点では実行されますが、将来、ルールが「興味を持っている」AST ノードと、実際に含まれるノードに基づいて、ルールの実行が必要かどうかを判定するロジックを Rust 側に追加する予定です。 これにより、無駄な呼び出しを回避し、パフォーマンスを向上させることができます。

上記の例では、ファイルに FunctionDeclaration が含まれていない場合、そのファイルに対するルール実行自体が完全にスキップされ、before フックもスキップ されます。

すべてのファイルで一度だけ必ず実行されるコードが必要な場合、Program のビジターオブジェクトを実装してください:

js
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つまで」ルールの例に戻ります:

js
const rule = {
  create(context) {
    let classCount = 0;

    return {
      ClassDeclaration(node) {
        classCount++;
        if (classCount === 6) {
          context.report({ message: "クラスが多すぎます", node });
        }
      },
    };
  },
};

create メソッドは、各ファイルごとに1回ずつ呼び出され、毎回新しい context オブジェクトが渡されます。

なぜこれが問題なのか?

最大限のパフォーマンスを実現するには、ルールが「関心を持つ」べきである静的な情報(例えば、どの種類のノードに関心があるか)を事前に知っている必要があります。その情報をもとに、次の2つの最適化が可能です:

  1. 通常、JS 側でアストを再帰的に走査する必要がない。代わりに、Rust 側でのアスト走査時に、関連するアストノードへの「ポインタ」リストを生成し、それを送信する。これにより、JS 側は全アストを探索するのではなく、直接関連ノードに「ジャンプ」できる。
  2. ファイル内にルールが関心を持つアストノード(この例ではクラス宣言)が まったく存在しない 場合は、そのファイルについてはそもそも JS 側への呼び出しをスキップできる。

しかし、JS は動的言語であり、create何でも 行える可能性があります。各呼び出しで完全に異なるビジターオブジェクトを返すことも可能です。そのため、create を呼んで初めて、「本当に create を呼び出す必要があるのか?」を確認しなければならないのです。

それに対し、代替 API では createOnce は一度だけ呼び出されるため、ルールの内容が確定します。これにより上記の最適化が可能になります。

明確に言うと、create API が、ESLint の設計上のミスだったわけではありません。ただし、一旦 Rust-JS の相互作用が考慮に入ると、いくつかの課題が生じます。

API 対応状況

Oxlint は、ほぼすべての ESLint API を対応しています:

  • アストの走査。
  • アストの探索(node.parentcontext.sourceCode.getAncestors)。
  • フィックス機能。
  • ルールオプション。
  • セレクタ(ESLint ドキュメント)。
  • SourceCode API(例: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 サーフェスをサポートする予定です。