ESLintとPrettierのコードリーディングでASTベースの静的解析を理解する

2025年11月12日(水)
平井 柊太
最終回の今回は、ESLintとPrettierの内部実装を読み解き、ASTと静的解析の処理フローを整理します。

はじめに

こんにちは。サイバーエージェントのソフトウェアエンジニアの平井柊太(@did0es)です。

前回は、ASTからTypeScriptのソースコードを生成しました。また、この生成の仕組みを利用して、第3回で実装したLinterをFormatterに作り変えました。

今回は、本連載の内容を振り返るとともに、開発者に広く用いられているLinterのESLintとFormatterのPrettierのコードを読み、その仕組みを深堀りしていきます。

本連載の振り返り

まず、各回の内容をかいつまんで振り返ります。

【第1回】「ASTの基礎知識とTypeScriptの環境構築から始めるCLIツール開発」

開発の準備として、本連載に登場する用語の整理と環境構築を行いました。前半では、以下の用語の指す意味を確認しました。

  • Linter:ソースコード上の問題を検出するツール
  • Formatter:ソースコードを整形するツール
  • ESLint:Linterの一種
  • Prettier:Formatterの一種
  • Biome:Linter・ Formatterの一種。ESLintやPrettierと部分的に互換性がある
  • Abstract Syntax Tree(AST):ソースコードを変換して得られる中間表現の一種。木構造のデータ
  • 静的解析:ソースコードを実行することなく解析する技術。LinterやFormatterに用いられている

特に、ASTについて「Acorn」というJSパーサーを用いてconsole.log関数をパースしたものからデータの構造を深堀りしました。以下のようなJSON形式で表現しました。

{
  "type": "Program",
  "start": 0,
  "end": 11,
  "body": [
	{
  	"type": "ExpressionStatement",
  	"start": 0,
  	"end": 11,
  	"expression": {
    	"type": "MemberExpression",
    	"start": 0,
    	"end": 11,
    	"object": {
      	"type": "Identifier",
      	"start": 0,
      	"end": 7,
      	"name": "console"
    	},
    	"property": {
      	"type": "Identifier",
      	"start": 8,
      	"end": 11,
      	"name": "log"
    	},
    	"computed": false,
    	"optional": false
  	}
	}
  ],
  "sourceType": "module"
}

以下の画像のような、入れ子の構造でも表現しました。

このASTを用いて行われる処理が静的解析でした。応用例として、ESLintのアーキテクチャを元に、どういった手順で静的解析によってソースコードが検査されるかを概観しました。

ESLintでは「Espree」というJSパーサーにより生成されたASTをEstraverseで走査し、Node.jsのEvent emitterで発火したイベント駆動で検査が行われていました。

後半では、asdfを用いた環境構築の方法を紹介しました。asdfによりNode.jsのバージョン管理を行いました。asdf経由でインストールしたv23.10.0のNode.jsでは、外部ライブラリなしでのTypeScriptの実行や実験的機能のNode.jsのコンフィグファイルが利用できました。併せて、macOS以外のユーザー向けにDockerを用いた環境構築の方法ご紹介しました。

以上の詳細は、第1回の記事からご覧ください。

【第2回】「TypeScript×Node.jsでCLIを開発してnpmで公開する基本手順を学ぶ」

第1回の環境を用いて、CLIの開発と公開を行いました。前半では、入力された文字列を反転して出力するCLIを実装しました。CLIの開発にはcommanderyargsのような非対話型CLI向けのライブラリ、enquirerpromptsのような対話型CLI向けのライブラリが広く用いられていました。

本連載ではreadlineなどのNode.jsの標準APIを用いて、これらのようなライブラリに依存しない形で非対話型CLIと対話型CLIを再現しました。実装の詳細はGitHubリポジトリの第2回のディレクトリをご覧ください。

また、開発時にTypeScriptのコンパイラ(tsc)で行っていた型チェックをtsgoで代替して速度が向上するか検証しました。結果は以下の通りで、3倍ほど実行速度が向上しました。

# tsc
$ npx tsc --noEmit --extendedDiagnostics
Files:                     	186
~~~
I/O Read time:           	0.01s
Parse time:              	0.09s
ResolveModule time:      	0.01s
ResolveTypeReference time:  0.00s
ResolveLibrary time:     	0.00s
Program time:            	0.13s
Bind time:               	0.05s
Check time:              	0.45s
printTime time:          	0.00s
Emit time:               	0.00s
Total time:              	0.62s

# tsgo
$ npx tsgo --noEmit --extendedDiagnostics
Files:          186
~~~
Config time: 	0.001s
Parse time:  	0.025s
Bind time:   	0.006s
Check time:  	0.136s
Emit time:   	0.000s
Total time:  	0.169s

後半では、実装したCLIをnpmに公開しました。npmのアカウント作成しCLIをtscでビルドした後に、npm CLIのpublishコマンドを用いてnpmのレジストリに反映しました。

以上の詳細は、第2回の記事からご覧ください。

【第3回】「ASTの解析とCLIの実装によるTypeScript製Lintツール開発」

第2回で実装したCLIを元にLinterを実装しました。前半では、Linterの実装の準備としてTypeScriptのASTを解析しました。ASTの生成にはTypeScript Compiler API(Compiler API)を利用しました。

入力されたソースコードを元に、Compiler APIで生成したASTから不要なプロパティを除き、以下のデータを出力するCLIを実装しました。

CLIでASTを解析した結果は、以下のように出力されました。

$ npm run dev -- ./analyzeAstCli.ts ./__mocks__/validInput.ts --debug

> 003-analyze-ast@1.0.0 dev
> node --experimental-default-config-file ./analyzeAstCli.ts ./__mocks__/validInput.ts

Welcome to the Analyze AST CLI!
# ~~~
# その他のdebugログ
# ~~~
[DEBUG] Kind: SourceFile, Text: class Animal {   spe
[DEBUG]   Kind: ClassDeclaration, Text: class Animal {   spe
[DEBUG]     Kind: Identifier, Text: Animal
[DEBUG]     Kind: PropertyDeclaration, Text: species: string;
[DEBUG]       Kind: Identifier, Text: species
[DEBUG]       Kind: StringKeyword, Text: string
# 続く...

後半では、前半の解析向けCLIを応用してLinterを実装しました。解析向けCLIにおけるASTノードの詳細を出力する処理について、ASTノードを順に検査する処理に置き換えました。検査には、以下の独自のルールを用いました。

  • "call-super-in-constructor": "error": 継承した先のクラスのconstructorでsuper()を呼び出していない場合エラーにする(ESLintのconstructor-superのようなルール)
  • "use-this-in-method": "error": クラスのメソッド内でthisを使っていない場合エラーにする(ESLintのclass-methods-use-thisのようなルール)

ルールは以下のようなJSON形式でファイルに記入し、CLIから読み取るようにしました。

{
  "call-super-in-constructor": "error",
  "use-this-in-method": "error"
}

CLIにinvalidInput.tsのような不正なファイルを渡すと、以下のようなエラーが出力されました。 CLIの実行結果:

$ npm run dev -- ./lintCli.ts ./__mocks__/invalidInput.ts

> 003-analyze-ast@1.0.0 dev
> node --experimental-default-config-file ./lintCli.ts ./__mocks__/invalidInput.ts

Welcome to the Lint CLI!
[ERROR] Constructor should call super() but does not
[ERROR] Method should use `this` but does not
invalidInput.ts:
class Animal {
  species: string;

  constructor(species: string) {
    this.species = species;
  }

  showSpecies() {
    console.log(`この動物は ${this.species} です。`);
  }

  eat(food: string) {
    console.log(`${this.species} は ${food} を食べています。`);
  }

  sleep(hours: number) {
    console.log(`${this.species} は ${hours} 時間眠ります。`);
  }

  move(distance: number) {
    console.log(`${this.species} は ${distance} メートル移動しました。`);
  }
}

class Dog extends Animal {
  name: string;
  age: number;

  constructor(
    name: string,
    age: number
  ) {
    this.name = name;
    this.age = age;
  }

  showInfo() {
    console.log(`名前: ${this.name}, 年齢: ${this.age}`);
  }

  bark() {
    console.log(`${this.name} が吠えました: ワンワン!`);
  }

  noThisMethod() {
    console.log('This method should not be called.');
  }
}

const pochi = new Dog('ポチ', 3);
pochi.showSpecies();                // この動物は Dog です。
pochi.eat('ドッグフード');          // Dog は ドッグフード を食べています。
pochi.sleep(8);                     // Dog は 8 時間眠ります。
pochi.move(10);                     // Dog は 10 メートル移動しました。
pochi.showInfo();                   // 名前: ポチ, 年齢: 3
pochi.bark();                       // ポチ が吠えました: ワンワン!

エラーがない場合は、以下のように出力されました。

$ npm run dev -- ./lintCli.ts ./__mocks__/validInput.ts

> 003-analyze-ast@1.0.0 dev
> node --experimental-default-config-file ./lintCli.ts ./__mocks__/validInput.ts

Welcome to the Lint CLI!
[INFO] No issues found

以上の内容の詳細は第3回の記事を、実装の詳細はGitHubリポジトリの第3回のディレクトリをご覧ください。

【第4回】「TypeScript Compiler APIでASTからコード生成しFormatter CLIを作る」

第3回で実装したLinterを元にFormatterを実装しました。前半では、Formatter実装の準備としてASTからTypeScriptのソースコードを生成しました。第3回に実装した解析処理の逆の処理に相当します。

ASTノードのkindごとに対応するTypeScriptの構文を構築し、Compiler APIのcreatePrinter関数でソースコードを生成するCLIを実装しました。

このCLIに以下のJSON形式のASTを入力すると、const greeting: string = "Hello, World!";が出力されます。

{
  "kind": "SourceFile",
  "children": [
    {
      "kind": "FirstStatement",
      "children": [
        {
          "kind": "VariableDeclarationList",
          "children": [
            {
              "kind": "VariableDeclaration",
              "children": [
                {
                  "kind": "Identifier",
                  "text": "greeting",
                  "children": []
                },
                {
                  "kind": "StringKeyword",
                  "text": "string",
                  "children": []
                },
                {
                  "kind": "StringLiteral",
                  "text": "\\"Hello, World!\\"",
                  "children": []
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "kind": "EndOfFileToken",
      "children": []
    }
  ]
}

後半では、以下のフローでソースコードを整形するFormatterを実装しました。

ソースコードからASTに落とし込み、各ASTノードを検査するまではLinterと同じです。その後、前半に実装したASTからソースコードを生成する処理を組み合わせ、ソースコードの整形を実現しました。整形には以下の独自ルールを用いました。

  • use-let-never-reassigned: 再代入されていないletconstに変換する(ESLintのprefer-constのようなルール)
  • double-quotes: ダブルクオートはシングルクオートに変換する(Prettierのquotesのようなルール)

ルールはLinterと同様、以下のようなJSON形式でファイルに記入し、CLIから読み取るようにしました。

{
  "use-let-never-reassigned": "error",
  "double-quotes": "error"
}

CLIにinvalidInput.tsのような不正なファイルを渡すと、以下のように整形されて出力されました。

CLIの実行結果:
$ npm run dev -- ./formatterCli.ts ./__mocks__/invalidInput.ts

> basic-project@1.0.0 dev
> node --experimental-default-config-file ./formatterCli.ts ./__mocks__/invalidInput.ts

Welcome to the Formatter CLI!
[INFO] [use-let-never-reassigned] letをconstに置換しました
[INFO] [double-quotes] ダブルクオートをシングルクオートに修正しました
[INFO] Formatted source:
const message: string = 'This method has fixable errors.';
console.log(message);
invalidInput.ts:
let message: string = "This method has fixable errors.";
console.log(message);

エラーがない場合は、以下のように整形されずに出力されました。

$ npm run dev -- ./formatterCli.ts ./__mocks__/validInput1.ts

> basic-project@1.0.0 dev
> node --experimental-default-config-file ./formatterCli.ts ./__mocks__/validInput1.ts

Welcome to the Formatter CLI!
[INFO] Formatted source:
const message: string = 'This method has fixable errors.';
console.log(message);

以上の内容の詳細は第4回の記事を、実装の詳細はGitHubリポジトリの第4回のディレクトリをご覧ください。

静的解析ツールのコードリーディング

これまでは簡易なLinterとFormatterを自らの手で再現し、その仕組みを概観してきました。本項では、広く用いられているLinterのESLintとFormatterのPrettierのコードから、さらに仕組みを深堀りしていきます。どちらもJavaScriptで実装されているので、JavaScriptの基本的な知識があれば読み進められます。

ESLintのコードリーディング

ESLintのコードリーディングを通して、一般的なLinterの処理のフローを理解します。コードはGitHubに公開されています。コミットID31d9392の実装を読み進めます。eslintコマンドが実行されると、以下の流れで処理が行われます。

図の番号ごとに処理を深堀ります。なお、今回はFlat Configに対応したESLintの処理を読み進めます。

【①bin/eslint.js】実装箇所
eslintコマンドで読み込まれ、実行されるファイルです。lib/cli.jsにあるCLIの処理を呼び出します。

initmcpのようなオプションが渡された場合、npxで外部のeslint関連のCLIを呼び出します。

【②lib/cli.js】実装箇所
ESLintクラスの処理を呼び出し、Lintを開始するファイルです。fixオプションのようなCLIに渡されているオプション固有の処理も、このファイルで行っています。

【③lib/eslint/eslint.js】実装箇所
ESLintクラスの実装が含まれているファイルです。ESLintクラスではコンストラクタでlinterを初期化し、これを元にクラスのメソッドでLintします。

【④lib/eslint/eslint.js lintFilesメソッド】実装箇所
ESLintクラスのメソッドのうち、ファイルごとにLintするlintFilesメソッドの実装を見てみましょう。

このメソッドでは、以下の順で処理が行われます。

  1. calculateWorkerCount関数(実装箇所)でスレッド数を決定
  2. スレッドが複数ある場合とない場合でそれぞれLintの処理を分岐

【⑤lib/eslint/eslint.js lintFilesWithoutMultithreading関数】実装箇所

スレッドが単一のときの処理を見てみましょう。④の2.の後にlintFilesWithoutMultithreading関数が実行されます。この関数でPromise.allでファイルごとに並行処理でlintFile関数が実行されます。

【⑥lib/eslint/eslint-helper.js lintFile関数】実装箇所
lintFile関数の処理を見てみましょう。この関数は、ファイルにverifyText関数(実装箇所)を実行してLintした結果を返します。

【⑦lib/linter/linter.js Linterクラス】実装箇所
⑥のverifyText関数では、あらかじめESLintクラスのコンストラクタで初期化されたlinterでLintします。linterの初期化にはLinterクラスから生成したインスタンスが用いられます(実装箇所)。

このクラスのverifyAndFixメソッド(実装箇所)で修正すべき対象がなくなるまでLintします。このメソッドでは、以下の順で処理が行われます。

  1. _verifyWithFlatConfigArrayメソッド(実装箇所): Flat Configの内容を読み込んで適用
  2. #flatVerifyWithoutProcessorsメソッド(実装箇所): Configにカスタムのプロセッサーがセットされていない場合、ESLint標準のJS向けのプロセッサーで入力ファイルを処理

【⑧lib/linter/linter.js runRules関数】
Lintのコアの処理がrunRules関数です。⑥の2.のメソッドで実行されます。この関数では、以下の順で処理が行われます。

  1. SourceCodeVisitorクラス(実装箇所): ソースコードの走査。addメソッドでセレクタ(ESLintがASTの操作に用いているESQueryの用語。ノードの識別子)に対してイベントリスナー登録
  2. FileContextクラス(実装箇所): Configのルールに基づいて問題を報告する用のContext作成
  3. SourceCodeクラス(実装箇所): traverseメソッドでASTを走査する手順(step)を設定
  4. SourceCodeTraverserクラス(実装箇所): traverseSyncメソッドで3.のstepを元にイベント発火・ルールの実行・検証を行う

特に4.では、ESLintの特徴であるイベント駆動のLintで効率の良いASTの走査とルールによる検証を実現しています。

以上の①〜⑧の工程で、ESLintはソースコードをLintします。

Prettierのコードリーディング

ESLintのコードリーディングと同様に、Prettierのコードリーディングを通して、一般的なFormatterの処理のフローを理解します。コードはGitHubに公開されています。コミットID5067313の実装を読み進めます。prettierコマンドが実行されると、以下の流れで処理が行われます。

図の番号ごとに処理を深堀ります。

【①bin/prettier.cjs】実装箇所
prettierコマンドで読み込まれ実行されるファイルです。src/cli/index.jsにあるCLIの処理(
実装箇所)を実行します。

experimental-cliフラグが渡された場合は、src/experimental-cli/index.jsにあるCLIの処理(実装箇所)を実行します。

【②src/cli/index.js run関数】実装箇所
CLIのオプションを引数として渡す形で、同ファイル内のmain関数を呼び出す関数です。

【③src/cli/index.js main関数】実装箇所
Formatを実行するformatFiles関数を実行する関数です。オプションの検証を行い、不正な場合エラーを投げる処理もこの関数で行います。

【④src/cli/format.js formatFiles関数】実装箇所
入力ファイルの内容やオプションを元に、ファイルごとにFormatを実行する関数です。この関数でFormatした結果のキャッシュも行います。

【⑤src/index.js format関数】実装箇所
④のformatFiles関数から呼び出される関数です。渡されたオプションごとの処理を行い、formatWithCursor関数を実行した結果を返します。

【⑥src/main/core.js formatWithCursor関数】実装箇所
coreFormat関数を実行しFormatする関数です。エディタ上でFormatした際にカーソルの位置を維持する処理も行います。

【⑦src/main/core.js coreFormat関数】実装箇所
Formatのコアの処理がcoreFormat関数です。この関数では、以下の順で処理が行われます。

  1. parseText関数(実装箇所): ソースコードの文字列をASTに変換
  2. printAstToDoc関数(実装箇所): ASTをDocという中間表現に変換
  • mainPrint関数とcallPluginPrintFunction関数が再帰状に実行される
    • mainPrint関数(実装箇所): 生成したdocをキャッシュ
    • callPluginPrintFunction関数(実装箇所): 出力向けのprinterの実行
  • printDocToStringWithoutNormalizedOptions関数(実装箇所): Docを元にフォーマットし、文字列に変換
    • 以下のDocのTYPEで分岐し、TYPEごとに決まったカテゴリの処理を適用(「カテゴリ」: DocのTYPE の形式で例示)
      • 「標準」: STRING, ARRAY, CURSOR
      • 「インデント」: INDENT, ALIGN, TRIM
      • 「レイアウト」: GROUP, FILL
      • 「条件分岐」: IF_BREAK, INDENT_IF_BREAK
      • 「改行」: LINE
      • 「行末」: LINE_SUFFIX, LINE_SUFFIX_BOUNDARY
      • 「メタ情報」: LABEL, BREAK_PARENT
    • 以上の①〜⑦の工程で、PrettierはソースコードをFormatします。

      おわりに

      本連載では、JavaScriptやTypeScript、ASTなどの技術を活用し、Linter・Formatterを自らの手で再現しました。また、広く用いられているLinterのESLintとFormatterのPrettierの内部実装を読み、仕組みを深堀りしました。

      本連載を通して、静的解析向けのツールがどういった技術を活用し、どのような仕組みで動くのか少しでも興味を持っていただけたら幸いです。最後までご精読いただき、ありがとうございました。

  • 株式会社サイバーエージェント グループIT推進本部 CIU
    2022年新卒入社。株式会社CAMでOSS開発やエンタメ関連のWebサービス開発に携わる。2023年CIUにJoin。プライベートクラウドのWeb UIの開発やWeb Speed Hachathon 2024の作問、CyberAgent Developer Conferenceの開発のリードを担当。Muddy WebやWeb Developer Conferenceなどで登壇。2024年サイバーエージェントのTypeScriptのNext Expertsに就任。
    現在は、FrontEnd Conference Tokyoの実行委員長やECMAScriptコミュニティのMeguro.esの主催、TypeScript関連のOSSコントリビュートに従事している。

    連載バックナンバー

    開発言語技術解説
    第5回

    ESLintとPrettierのコードリーディングでASTベースの静的解析を理解する

    2025/11/12
    最終回の今回は、ESLintとPrettierの内部実装を読み解き、ASTと静的解析の処理フローを整理します。
    開発言語技術解説
    第4回

    TypeScript Compiler APIでASTからコード生成しFormatter CLIを作る

    2025/9/11
    第4回の今回は、TypeScript Compiler APIでASTからのコード生成とFormatter CLIの実装を解説します。
    開発言語技術解説
    第3回

    ASTの解析とCLIの実装によるTypeScript製Lintツール開発

    2025/7/30
    第3回の今回は、抽象構文木(AST)の解析を通じて、Lintルールを実装するCLIツールの開発手順について解説します。

    Think ITメルマガ会員登録受付中

    Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

    Think ITメルマガ会員のサービス内容を見る

    他にもこの記事が読まれています