🧊

OxcのLinter、`oxlint`のコードを読む

Linter | The JavaScript Oxidation Compiler https://oxc-project.github.io/docs/guide/usage/linter.html

コントリビュートした記念としても、記録を残しておこうかと。

Oxcとoxlint

oxc-project/oxc: ⚓ A collection of JavaScript tools written in Rust. https://github.com/oxc-project/oxc

Oxcって名前は、Rustで書かれたJS向けツールセット群の総称みたいなもの。

  • Linter
  • Parser
  • Resolver
  • Formatter
  • Transformer
  • Minifier
  • etc…

みたく手広くカバーしてて、eslintの置き換え(完全互換ではないが50x-100x速い)を目指してるのが、oxlintってコマンドとして使える。

Oxlint General Availability | The JavaScript Oxidation Compiler https://oxc-project.github.io/blog/2023-12-12-announcing-oxlint.html

というわけで、npx oxlintコマンドを実行したときのコードの流れを読んでいく。

読んでるソースコードは、oxlint_v0.2.0のタグが切られた時点。

npm/oxlint/bin

Rustのプロジェクトとはいえ、npxで呼べるようにしてる以上、Node.jsのエンドポイントがある。

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/npm/oxlint/bin/oxlint#L22

それがココではあるものの、中は事前にビルドしたバイナリに丸投げしてるだけ。

crates/oxc_cli

oxlintバイナリは、oxc_cliというクレートの、src/linter/main.rscargo buildしたもの。

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_cli/Cargo.toml#L28

ここでも、引数をパースしてからLintRunnerrun()するだけ。

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_cli/src/lint/main.rs#L17-L19

実行フローとしてのメインはココ。

  • Lintする対象のパスをかき集め
  • 拡張子やプラグインを精査
  • LintServiceのインスタンスを作る
  • DiagnosticServiceのインスタンスも作る
  • rayonで別スレッドを立て
    • DiagnosticServiceのSender(mpsc::channel)を取得し
    • LintServicerun()に渡して実行
  • DiagnosticServicerun()して結果を表示

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_cli/src/lint/mod.rs#L26

crates/oxc_diagnostics

LintServiceに渡したmspc::channelのSenderに対応するReceiverでrecv()してLint結果を待つ役。

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_diagnostics/src/service.rs#L96

何かメッセージが送られてきたら、それをフォーマットして表示してる。

ちなみに、整形して出力する部分のフォーマットは、中でも使ってるmietteというクレートを参考にしてるらしい。

zkat/miette: Fancy extension for std::error::Error with pretty, detailed diagnostic printing. https://github.com/zkat/miette

Noneが飛んできたら終了。

crates/oxc_linter

まずはLintServiceから。

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_linter/src/service.rs#L51

crates/oxc_linter: Runtime

Runtime: process_path()

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_linter/src/service.rs#L162

  • パスから、拡張子と中身を推定
    • 現時点で対応してるのは.[m|c]?[j|t]s or .[j|t]sx
  • 例外として、.vue.astro.svelteは、scriptブロックだけ部分的に対応
  • ソースとしては、JavaScriptとTypeScript
  • それぞれのソースに対して、Lintを実行していくのがprocess_source()
  • Fixのオプションが指定されてる場合は、Fix結果を保存
  • Lint結果が返ってきた場合、それをDiagnosticServicewrap_diagnostics()して送信

Runtime: process_source()

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_linter/src/service.rs#L206

  • ソースをParserで処理してASTへ
    • Oxcとしての本懐はこの先だが、今回はスキップ
  • SemanticBuilderからLintContextを作って、Linterrun()

crates/oxc_semantic: SemanticBuilder

SemanticBuilderは、そのソース全体を表す表現。

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_semantic/src/builder.rs#L156

  • source_text: ソース本文
  • nodes: パースされたASTのノード
  • classes: クラス
  • scopes: スコープ
  • trivias: コメントなど
  • jsdoc: JSDoc
  • etc

といったものを抱えてる。

パースされたASTそれだけでは、ただのJSONみたいなもんであり、それを走査する術を持たない。愚直にツリーを歩いていくのも大変でしょ?ってことで、nodes().iter()みたいなショートカットが用意されてる。 あとはコメントみたくASTとして表現されないものを抱えたり。

SemanticBuilderbuild()すると、SemanticBuilderReturnが生成されるけど、LintContextに渡されるのはSemanticBuilderReturnSemanticという構造体だけ。

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_semantic/src/lib.rs#L34

crates/oxc_linter: LintContext

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_linter/src/context.rs#L14

いわゆるコンテキスト。

抱えるSemanticが本体で、その各情報に対するGetterを持ちつつ、Lint結果に問題があったことを知らせるためのdiagnostic()diagnostic_with_fix()も生えてる。

crates/oxc_linter: Linter

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_linter/src/lib.rs#L140

このLinterrun()が、Lint処理としての本丸。

  • Linterは、self.rulesに、対象ソースに対して実行するルールたちを保持してる
    • これがeslint/no_debuggerとかtypescript/prefer_as_constとか見慣れたルール名
  • 各ルールは、必要に応じて3パターンの処理を実装できる
  • この3パターンを順に実行する

今の時点で実装されてるルールを知るには、この一覧を見る。

https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_linter/src/rules.rs

新規でルールを実装した場合、忘れずにココに足す。

まとめ

  • Rustだがコードは読みやすい
  • Linterというドメイン知識がやはり大事
    • Linterというものの実装パターンとか
    • ASTにどういうものがあり、どういう用途に対応してるかとか

Linterを作る場合の、最小構成のコードも公開されてる。

https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/examples/linter.rs

Linterの外側、つまり世界最速である所以があるあたりは、また違った見どころの宝庫なのであろうな・・・。(不慣れドメイン過ぎて理解できる気はしない)

単に新規ルールを追加しただけコントリビューター的な観点としては、

  • 材料(ライブラリ)が揃っていて
  • ただルールを書けばいいだけの状態なら
  • Rustの基礎文法ができればPRは出せる

という感じ。

設計や方針はeslint互換なので、基本的にはJSのコードをRustにポートすればいいことがほとんどなので。

ただ材料が揃ってないなど、それ以上のことをやろうとする場合は、

  • それを実装するRust力
  • コミュニティとしてどういう方針なのか空気を読み、必要に応じて判断を委ねるなどのコミュ力

あたりが当然ながら必要になってきて、ちょっと敷居が上がるな〜って感じ。

直近だと、

といったあたりがWIPであり足りない材料で、情報を取りに行くのがちょっと難しいなと感じたところ。