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.rsをcargo buildしたもの。
https://github.com/oxc-project/oxc/blob/oxlint_v0.2.0/crates/oxc_cli/Cargo.toml#L28
ここでも、引数をパースしてからLintRunnerをrun()するだけ。
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)を取得しLintServiceをrun()に渡して実行
DiagnosticServiceをrun()して結果を表示
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から。
self.runtimeにArc<Runtime>だけ持つRuntimeはLint対象のパスを抱えてるrun()すると、Runtimeのpathsのイテレータを、rayonのpar_bridgeで並列実行- 最後に
Noneを送っておしまい
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]sor.[j|t]sx
- 現時点で対応してるのは
- 例外として、
.vueと.astroと.svelteは、scriptブロックだけ部分的に対応 - ソースとしては、JavaScriptとTypeScript
- それぞれのソースに対して、Lintを実行していくのが
process_source() - Fixのオプションが指定されてる場合は、Fix結果を保存
- Lint結果が返ってきた場合、それを
DiagnosticServiceのwrap_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を作って、Linterでrun()
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として表現されないものを抱えたり。
SemanticBuilderでbuild()すると、SemanticBuilderReturnが生成されるけど、LintContextに渡されるのはSemanticBuilderReturnのSemanticという構造体だけ。
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
このLinterのrun()が、Lint処理としての本丸。
Linterは、self.rulesに、対象ソースに対して実行するルールたちを保持してる- これが
eslint/no_debuggerとかtypescript/prefer_as_constとか見慣れたルール名
- これが
- 各ルールは、必要に応じて3パターンの処理を実装できる
run_once(): 一度だけ実行されるrun_on_symbol(): シンボルごとに実行されるrun(): ノードごとに実行される- というのがTraitになってる
- この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力
- コミュニティとしてどういう方針なのか空気を読み、必要に応じて判断を委ねるなどのコミュ力
あたりが当然ながら必要になってきて、ちょっと敷居が上がるな〜って感じ。
直近だと、
- JSの正規表現
aria-query- 各ASTノードにおける制御フローのグラフ
といったあたりがWIPであり足りない材料で、情報を取りに行くのがちょっと難しいなと感じたところ。