Slide 1

Slide 1 text

Plugin System in Rust based JavaScript / TypeScript Linters 1 Rust製JavaScript / TypeScript Linterにおける プラグインシステムの裏側 TSKaigi 2025 @unvalley_

Slide 2

Slide 2 text

- Biome Member - Work at FRAIM Rust / TS - building ephe.app Note OSS 2 @unvalley_

Slide 3

Slide 3 text

3 ESLint

Slide 4

Slide 4 text

4 ESLint Plugin System - ESLintの特徴はPlugin System - ルールのみならずParserもPlugable - ESTree互換のあるParser(@typescript-eslint/parser 他)を利用可能 - これによってTypeScript, Babel などの構文に対応

Slide 5

Slide 5 text

5 typescript-eslint eslint-plugin-unicorn eslint-plugin-react … Lint Rules for Your Team Private Public ESLint Plugins Custom Lint Rules)

Slide 6

Slide 6 text

6 ESLint v9.0.0, Flat Config の時代へ Retrospective: https://eslint.org/blog/2025/05/eslint-v9.0.0-retrospective/

Slide 7

Slide 7 text

7 Rust meets Web Toolchain…

Slide 8

Slide 8 text

8 Biome Oxlint deno_lint

Slide 9

Slide 9 text

9 Biome Oxlint deno_lint 独自のCST (→ AST) 複数言語 swcベースのAST Deno同梱 Rust用AST, JS用のESTree互換AST 互換性重視 / / /

Slide 10

Slide 10 text

Rust製LinterでこれまでPluginが提供されていなかった理由 10 パフォーマンス JS / TSでPluginを記述可能にするには、JS/Rust間のデータ転送 コストによるパフォーマンス懸念の解消、またはそれが不要な方法 を選びたい。 コアへ集約する思想 コミュニティで確立されたLint Rulesなどをコアに集約する形で始 まったプロジェクトが多い。どれもESLintコミュニティで人気なプラ グインは可能な限り移植している。

Slide 11

Slide 11 text

Rust製JS/TS LinterがPluginを提供する手段 11 Rust DSL JavaScript / TypeScript WASM

Slide 12

Slide 12 text

Rust製JS/TS LinterがPluginを提供する手段 12 Rust DSL JavaScript / TypeScript WASM

Slide 13

Slide 13 text

13 Biome (v2.0) Oxlint deno_lint (v2.2.0 Experimental Future

Slide 14

Slide 14 text

14 Biome (v2.0) Oxlint deno_lint (v2.2.0 Experimental Future 2025年5月時点で、biome, deno_lintがプラグインシステムをbeta導入

Slide 15

Slide 15 text

15 Biome (v2.0) Oxlint deno_lint (v2.2.0 Experimental Future GritQLベースのPlugin実装の裏側と JS/TS APIのために必要なものについて

Slide 16

Slide 16 text

RFC Biome plugins 16 https://github.com/biomejs/biome/discussions/1649

Slide 17

Slide 17 text

GritQL 17 コードベースに対して構造的な検索・修正を可能にする 強力なOSSのクエリ言語。 Grit.ioという会社からHoneycomb傘下へ

Slide 18

Slide 18 text

no-object-assign (biome plugin version) 18 Object.assign の利用箇所を検知 `$fn($args)` where { $fn <: `Object.assign`, register_diagnostic( span = $fn, message = "Use object spread syntax" ) }

Slide 19

Slide 19 text

no-object-assign (biome plugin version) 19 Object.assign の利用箇所を検知 `$fn($args)` where { $fn <: `Object.assign`, register_diagnostic( span = $fn, message = "Use object spread syntax" ) } ASTを知らなくてもPluginを書ける!

Slide 20

Slide 20 text

no-console (biome plugin version) 20 console.{log,info,warn,error} の利用箇所を検知 `console.$method($message)` where { $method <: or { `log`, `info`, `warn`, `error` }, register_diagnostic( span = $fn, message = "Don’t use console" ) }

Slide 21

Slide 21 text

Biome Linter Plugins 21 https://next.biomejs.dev/linter/plugins/

Slide 22

Slide 22 text

なぜBiomeはGritQL(DSL)を選んだのか 22 - 構造的な検索に加えて修正が可能であり、他DSLと比較して文法が簡潔 - GritQLはRust製なので、プラグインシステムの実装がRust内に完結でき、 パフォーマンスを損なわない - ASTの詳細を知らなくてもPluginを書ける(ルール内容に依存) - 将来的な JS/TS API の可能性(github.com/honeycombio/gritql/discussions/403) - Gritチームの積極的な協力とOSS化

Slide 23

Slide 23 text

Biome GritQL Plugin 実行の流れ 23 1. biome.json にて、Pluginの読み込み・解決 2. GritQLの構文解析・実行可能な形式へコンパイル 3. Analyzerへのプラグイン登録(→ 内部でキャッシュ保持) 4. Lint対象ファイル(e.g. JS, TS)の構文解析(→ CSTの構築) 5. GritQL Plugin Lint Rule実行(CST各ノードに対しGrtiQLパターンマッチ) 6. 結果の出力

Slide 24

Slide 24 text

24 Biome (v2.0) Oxlint deno_lint (v2.2.0 Experimental Future

Slide 25

Slide 25 text

Biome Plugin with JavaScript Engine (future) 25 - JS / TSでプラグインを書く場合は、その実行環境が必要 - いくつかの選択肢がある中で検討中 - V8, Spider Monkey, JavaScriptCore, QuickJS, Boa, Nova 他 - Custom Engineという選択肢も0%ではない

Slide 26

Slide 26 text

26 JS / TS Plugins JavaScript Engine Rust Linter Linter User

Slide 27

Slide 27 text

27 JS / TS Plugins JavaScript Engine Rust Linter Linter User

Slide 28

Slide 28 text

28 Biome Biome Plugins は Lead Member : Arend (github.com/arendjr) の功績。 V2 coming soon!

Slide 29

Slide 29 text

29 Biome (v2.0) Oxlint deno_lint (v2.2.0 Experimental Future

Slide 30

Slide 30 text

30 Biome (v2.0) Oxlint deno_lint (v2.2.0 Experimental Future JS/TS Plugins 実装上の工夫について

Slide 31

Slide 31 text

no-object-assign (deno_lint plugin version, 省略あり) 31 Object.assign の利用箇所を検知 create(ctx) { return { MemberExpression(node) { if (node.object.type === "Identifier" && node.property.type === "Identifier") { if (node.object.name === "Object" && node.property.name === "assign") { ctx.report({ node, message: "Use object spread syntax”}) } // … };

Slide 32

Slide 32 text

no-object-assign (deno_lint plugin version, 省略あり) 32 Object.assign の利用箇所を検知 create(ctx) { return { MemberExpression(node) { if (node.object.type === "Identifier" && node.property.type === "Identifier") { if (node.object.name === "Object" && node.property.name === "assign") { ctx.report({ node, message: "Use object spread syntax”}) } // … }; ESLintに近い書き方が可能!ASTを知ってたら直感的

Slide 33

Slide 33 text

Deno Docs Lint Plugins 33 https://docs.deno.com/runtime/reference/lint_plugins/

Slide 34

Slide 34 text

34 JS / TS Plugins JavaScript Engine Rust Linter Linter User

Slide 35

Slide 35 text

35 JS / TS Plugins JavaScript Engine (deno runtime) Rust Linter Linter User V8 deno_lintはDenoで動くという前提と rusty_v8の存在があるためJavaScript Engineは決定的

Slide 36

Slide 36 text

36 JS / TS Plugins JavaScript Engine (deno runtime) Rust Linter Linter User V8

Slide 37

Slide 37 text

Speeding up the JavaScript ecosystem - Rust and JavaScript Plugins 37 https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-11/

Slide 38

Slide 38 text

38 JavaScript Engine (deno runtime) Rust あの 5万行の TypeScript checker.ts (約3MB で2.91秒 ASTが非常に大きく、その処理は重い 1 AST  JSON by serde_json V8 2 JSON.parse() 課題:Serialize in Rust, Deserialize in JS is HEAVY

Slide 39

Slide 39 text

39 JavaScript Engine (deno runtime) Rust 1 AST  JSON by serde_json V8 2 JSON.parse() Deserialization による根本的なオーバーヘッドを排除するために、AST をフラット化(効率良い Uint8Array に)する方法を採用 解決方法:Flattening the AST

Slide 40

Slide 40 text

ASTのフラット化 40 1. IDによる参照(→ AST配列化) 2. プロパティの分離(→ ノード形状を統一) 3. 文字列テーブル(→ 最適化)

Slide 41

Slide 41 text

単純なASTを考える(もっと大きいので、このままだと重い) 41 if (condition) { foo(); } const ast = { type: "IfStatement", test: { type: "Identifier", name: "condition" }, consequent: { type: "BlockStatement", body: [{ type: "ExpressionStatement", expression: { type: "CallExpression" } }] }, alternate: null };

Slide 42

Slide 42 text

ASTのフラット化 42 1. IDによる参照(→ AST配列化) 2. プロパティの分離(→ ノード形状を統一) 3. 文字列テーブル(→ 最適化)

Slide 43

Slide 43 text

43 const ast = [ { type: "" }, // index 0:ダミー { type: "IfStatement", // index 1 test: 2, // ノードをindexで参照 consequent: 3, alternate: 0 }, { type: "Identifier", … }, // index 2 { type: "BlockStatement", body: [4] }, // index 3 { type: "ExpressionStatement" … } // index 4 ]; IDによる参照

Slide 44

Slide 44 text

ASTのフラット化 44 1. IDによる参照(→ AST配列化) 2. プロパティの分離(→ ノード形状を統一) 3. 文字列テーブル(→ 最適化)

Slide 45

Slide 45 text

45 const ast = { properties: [ {}, { test: 2, consequent: 3, alternate: 0 }, { name: "condition" }, { body: [4] }, { expression: 5 } ], nodes: [ // ノードを type, child, next, parentのみを持つようにして形状を統一 { type: "", child: 0, next: 0, parent: 0 }, { type: "IfStatement", child: 2, next: 0, parent: 0 }, { type: "Identifier", child: 0, next: 3, parent: 1 }, { type: "BlockStatement", child: 4, next: 0, parent: 1 }, { type: "ExpressionStatement", child: 5, next: 0, parent: 3 }, { type: "CallExpression", child: 0, next: 0, parent: 4 } ]}; プロパティの分離

Slide 46

Slide 46 text

46 const ast = { properties: [ {}, { test: 2, consequent: 3, alternate: 0 }, { name: "condition" }, { body: [4] }, { expression: 5 } ], nodes: [ // ノードを type, child, next, parentのみを持つようにして形状を統一 { type: "", child: 0, next: 0, parent: 0 }, { type: "IfStatement", child: 2, next: 0, parent: 0 }, { type: "Identifier", child: 0, next: 3, parent: 1 }, { type: "BlockStatement", child: 4, next: 0, parent: 1 }, { type: "ExpressionStatement", child: 5, next: 0, parent: 3 }, { type: "CallExpression", child: 0, next: 0, parent: 4 } ]}; プロパティの分離 ノードの巡回が簡単になる 1. IfStatement を訪問 2. 子の 2 Identifier を訪問 3. 兄弟の 3 : BlockStatement を訪問 4. 子の 4 ExpressionStatement を訪問

Slide 47

Slide 47 text

ASTのフラット化 47 1. IDによる参照(→ AST配列化) 2. プロパティの分離(→ ノード形状を統一) 3. 文字列テーブル(→ 最適化)

Slide 48

Slide 48 text

48 const ast = { stringTable: ["", "IfStatement", "Identifier", "BlockStatement"], properties: [/* 省略 */], nodes: [ { type: 0, child: 0, next: 0, parent: 0 }, { type: 1, child: 2, next: 0, parent: 0 }, // IfStatement { type: 2, child: 0, next: 3, parent: 1 }, // Identifier { type: 3, child: 4, next: 0, parent: 1 }, // BlockStatement // ... ] }; 文字列テーブルによる最適化

Slide 49

Slide 49 text

49 const ast = { stringTable: ["", "IfStatement", "Identifier", "BlockStatement"], properties: [/* 省略 */], nodes: [ // type, child, next, parent 0,0,0,0, 1,2,0,0, // IfStatement 2,0,3,1, // Identifier 3,4,0,1, // BlockStatement // ... ] }; 文字列テーブルによる最適化 ノードの位置は index * 4 で簡単に計算可能 index 2のノードの開始位置は、248 ですぐに見つけられる

Slide 50

Slide 50 text

ASTのフラット化 50 1. IDによる参照(→ AST配列化) 2. プロパティの分離(→ ノード形状を統一) 3. 文字列テーブル(→ 最適化)

Slide 51

Slide 51 text

51 JavaScript Engine Rust 1 Flat AST V8 2 Flat ASTを巡回して Lintルール適用 ASTのフラット化 ASTのフラット化と(未紹介の)遅延Deserializeによって、 実行速度は 0.62秒(約4.7倍高速化)になり、メモリ使用量の削減も

Slide 52

Slide 52 text

52 deno_lint

Slide 53

Slide 53 text

53 Biome (v2.0) Oxlint deno_lint (v2.2.0 Experimental Future

Slide 54

Slide 54 text

Oxlint Plugins Written in JavaScript 54 https://github.com/oxc-project/oxc/discussions/10342

Slide 55

Slide 55 text

55 Biome (v2.0) Oxlint deno_lint (v2.2.0 Experimental Future

Slide 56

Slide 56 text

56 Biome Interested? Sponsor us! Weʼre still community-based.

Slide 57

Slide 57 text

References 57 - github.com/biomejs/biome - github.com/denoland/deno - github.com/oxc-project/oxc - Speeding up the JavaScript ecosystem - Rust and JavaScript Plugins - Deep Dive into deno lint plugin | ドクセル

Slide 58

Slide 58 text

Plugin System in Rust based JavaScript / TypeScript Linters 58 Rust製JavaScript / TypeScript Linterにおける プラグインシステムの裏側 TSKaigi 2025 @unvalley_