Back

AST の内部:ツールがコードを理解する仕組み

AST の内部:ツールがコードを理解する仕組み

ESLint がコードを実行する前に問題を指摘したり、Prettier が保存時に乱雑な関数を即座に整形したりするたびに、その裏側では正確な処理が行われています。これらのツールは、ソースコードをテキストとして読んでいるわけではありません。構造化されたツリーとして読んでいるのです。そのツリー、つまり 抽象構文木(AST) を理解することで、ほぼすべての現代的な開発者ツールがどのように動作しているかが説明できます。

重要なポイント

  • AST は、コードの構造をツリー形状で表現したもので、空白文字やグループ化のための括弧などの表層的な詳細は省略されることが多い。
  • リンター、フォーマッター、コンパイラはすべて、ソースコードを AST にパースし、それを走査して、各ノードでルールや変換を適用することで動作する。
  • ビジターパターンが主流のアプローチ:ツールは特定のノードタイプに対してハンドラーを登録し、ツリーが走査される際に反応する。
  • 具象構文木(CST)はすべてのトークンを保持するため、インクリメンタルパースと完全な忠実性が必要なエディターに適している。
  • Biome や Oxc のような Rust ベースのツールは、同じパース・走査・実行モデルを使用しながらも、多くの JavaScript ベースのパーサーをはるかに上回るパフォーマンスを実現している。

抽象構文木とは何か?

ツールがソースコードをパースする際、2つの段階を経ます。

まず、字句解析器(レクサー) (またはトークナイザー)が生のテキストをトークンに分解します:キーワード、識別子、演算子、句読点などです。次に パーサー がそれらのトークンを受け取り、コードの文法構造を表すツリーを構築します。

そのツリーが AST です。「抽象的」と呼ばれるのは、空白文字、ほとんどの句読点、純粋にグループ化のために使われる括弧などの表層的な詳細を省略する一方で、意味的に重要なものを保持するためです。

次の行を見てみましょう:

const x = 5 + 3

パーサーは次のようなものを生成します:

{
  "type": "VariableDeclaration",
  "kind": "const",
  "declarations": [{
    "type": "VariableDeclarator",
    "id": { "type": "Identifier", "name": "x" },
    "init": {
      "type": "BinaryExpression",
      "operator": "+",
      "left":  { "type": "Literal", "value": 5 },
      "right": { "type": "Literal", "value": 3 }
    }
  }]
}

すべてのノードには type があります。子ノードは親ノードの内部にネストされます。プログラム全体は Program ノードをルートとするツリーになります。

AST Explorer を使えば、これを直接探索できます。任意の JavaScript または TypeScript コードを貼り付けて、生成されたツリーをリアルタイムで確認できます。

リンターとフォーマッターが AST を使う方法

ツールが AST を取得すると、有用なことができます:走査する ことです。

ほとんどのツールは ビジターパターン を使用します。特定のノードタイプに対して関数を登録すると、走査エンジンは一致するノードに遭遇するたびにその関数を呼び出します。

ESLint はまさにこの方法で動作します。各リントルールはビジターオブジェクトです。ESLint が AST を走査すると、各ノードで関連するルールハンドラーを呼び出します。=== の代わりに == を禁止するルールは、単に BinaryExpression ノードをリッスンして operator プロパティをチェックするだけです。

Babel はコード変換に同じアプローチを使用します。ソースを AST にパースし、ノードを変更するビジターベースの変換を適用し、変更されたツリーをソースコードに 出力 します。これが、モダンな JavaScript 構文を古い同等のものにコンパイルする方法です。

Prettier は異なる角度からアプローチします。コードを AST にパースし、元のフォーマットをすべて破棄し、独自のレイアウトルールに従ってツリーを再出力します。AST が真実の源であり、元のテキストではありません。

AST vs. CST:構造がより重要な場合

AST は、構文的には必要だが意味的には冗長なトークンを省略します。具象構文木(CST) は、ソースコードの完全な構文構造を保持し、トークンとその位置を保持します。

Tree-sitter は、インクリメンタルパース に最適化された具象構文木を生成します。編集によって影響を受けるツリーの部分だけを更新できるため、Neovim や Zed のようなモダンなエディター内でのシンタックスハイライト、コード折りたたみ、構造的編集に適しています。

高性能パースへのシフト

JavaScript エコシステムの新しいツールは、パースパフォーマンスを大幅に向上させています。BiomeOxc のようなプロジェクトは Rust で実装されており、独自のパーサーと AST 表現をゼロから構築しています。

これらは、JavaScript ベースのパーサーを上回る速度でリンティングとフォーマッティングを処理しながら、インポート属性や最新の TypeScript 機能などのモダンな構文をサポートしています。

基本的なモデルは同じです — ツリーにパースし、走査し、分析または変換を適用する — しかし、実装はスケールに最適化されています。

まとめ

カスタム ESLint ルールを書いている場合でも、jscodeshift でコードモッドを構築している場合でも、あるいはツールがなぜそのように動作するのかを理解しようとしている場合でも、AST は見るべき正しい場所です。これは、すべての本格的な開発者ツールが構築される構造化された表現です。構文木を読めるようになれば、リンター、フォーマッター、コンパイラの動作は魔法のように感じられなくなります。

よくある質問

日常的な使用には必要ありません。これらのツールは適切なデフォルト設定ですぐに動作します。しかし、カスタムリントルールを書いたり、コードモッドを構築したり、予期しないツールの動作をデバッグしたりする場合、AST がコードをどのように表現しているかを理解することで、ツールが各ステップで実際に何をしているかについての明確なメンタルモデルが得られます。

AST は、構文的には必要だが意味的には冗長なトークン(カンマ、括弧、空白文字など)を取り除きます。CST はソースコードの完全な構文構造を保持します。CST は、完全な忠実性とインクリメンタルパースが重要なエディターや IDE で好まれ、AST は通常、リンター、コンパイラ、フォーマッターで使用されます。

最も簡単な方法は astexplorer.net の AST Explorer です。任意の JavaScript または TypeScript スニペットを貼り付け、acorn、babel、typescript などのパーサーを選択すると、ツールが完全なツリーをリアルタイムで表示します。他の言語もサポートしており、ブラウザ内で直接変換を試すこともできます。

Rust はこれらのツールにメモリの割り当てとレイアウトの直接的な制御を与え、ガベージコレクションの一時停止を回避し、高度に最適化されたネイティブコードにコンパイルされます。つまり、パース、走査、分析のすべてが大幅に高速に実行されます。これは、数千のファイルを持つ大規模なコードベースで特に顕著になります。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay