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]s
or.[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であり足りない材料で、情報を取りに行くのがちょっと難しいなと感じたところ。