T-CREATOR

ESLint と AST 入門:ESTree/Token/SourceCode を 10 分で把握

ESLint と AST 入門:ESTree/Token/SourceCode を 10 分で把握

ESLint のカスタムルールを作成しようとして、「AST」「ESTree」「Token」といった用語に戸惑った経験はありませんか。 これらは ESLint がコードを解析する上で欠かせない基礎概念ですが、公式ドキュメントを読んでもなかなか全体像が掴みづらいものです。

本記事では、ESLint の内部で活躍する AST(抽象構文木)の基本から、ESTree 仕様、Token、SourceCode オブジェクトまでを、初心者の方にもわかりやすく解説します。 10 分程度で読み終わる内容にまとめましたので、ESLint のカスタムルール作成に挑戦したい方は、ぜひ最後までお付き合いください。

背景

ESLint がコードを解析する仕組み

ESLint は、JavaScript や TypeScript のコードをチェックするための静的解析ツールです。 私たちが書いたコードを「文字列」として読み込むだけでは、その構造や意味を理解できません。

そこで ESLint は、コードを**AST(Abstract Syntax Tree:抽象構文木)**という木構造のデータに変換してから解析を行います。 AST に変換することで、コードの各要素(変数宣言、関数呼び出し、条件分岐など)を体系的に扱えるようになるのです。

AST とは何か

AST は、プログラムのソースコードを木構造で表現したものです。 たとえば const x = 5 + 3; というコードは、以下のような構造で表されます。

  • ルートノード(プログラム全体)
    • 変数宣言ノード
      • 変数名 x
      • 初期値(二項演算)
        • 左辺 5
        • 演算子 +
        • 右辺 3

この木構造により、ESLint は「この変数は宣言されているか」「この演算子は正しく使われているか」といったチェックを機械的に行えます。

下図は、ESLint がソースコードを AST に変換し、ルールで検証する基本的なフローを示しています。

mermaidflowchart LR
  source["ソースコード<br/>(文字列)"] -->|パース| ast["AST<br/>(木構造)"]
  ast -->|トラバース| rules["ESLintルール<br/>(検証)"]
  rules -->|違反検出| output["エラー/警告"]

このフローを理解することで、ESLint がどのようにしてコードの問題を見つけ出しているかがわかります。

ESTree という標準仕様

JavaScript の AST にはさまざまな表現方法がありますが、ESLint ではESTreeという仕様を採用しています。 ESTree は JavaScript AST の事実上の標準で、各ノードの型や構造が明確に定義されているのです。

たとえば、変数宣言は VariableDeclaration ノード、関数宣言は FunctionDeclaration ノードといった具合に、すべての構文要素に対応するノードタイプが決められています。 この統一された仕様のおかげで、ESLint のルール作成者は一貫した方法でコードを解析できるようになりました。

課題

カスタムルール作成の壁

ESLint には多くの組み込みルールがありますが、プロジェクト固有のコーディング規約をチェックしたい場合は、カスタムルールを作成する必要があります。 しかし、いざカスタムルールを書こうとすると、以下のような疑問に直面するでしょう。

  • 「どのノードタイプを監視すればいいのか」
  • 「ノードの構造はどうなっているのか」
  • 「トークンとノードの違いは何か」
  • 「SourceCode オブジェクトで何ができるのか」

これらの疑問に答えるには、AST の基礎知識が不可欠です。

ドキュメントの情報量の多さ

ESLint の公式ドキュメントや ESTree の仕様書は非常に詳細ですが、初心者にとっては情報量が多すぎて、どこから手をつければよいかわかりにくい面があります。 また、用語の定義が散らばっていて、全体像を掴むのに時間がかかるという課題もあるのです。

下図は、カスタムルール作成時に理解が必要な要素とその関係性を示しています。

mermaidflowchart TB
  rule["カスタムルール"] -->|監視| node["ASTノード<br/>(ESTree)"]
  rule -->|参照| token["Token<br/>(字句要素)"]
  rule -->|利用| sourcecode["SourceCode<br/>(ヘルパー)"]

  node -->|構成| identifier["Identifier"]
  node -->|構成| literal["Literal"]
  node -->|構成| expression["Expression"]

  sourcecode -->|提供| api["コメント取得<br/>スコープ解析<br/>テキスト取得"]

この図からわかるように、カスタムルールを作成するには、ノード・トークン・SourceCode という 3 つの要素を理解する必要があります。

実践的な学習リソースの不足

AST や ESTree について解説した記事は多くありますが、「ESLint のカスタムルール作成」という実践的な観点からまとめられた日本語リソースは限られています。 特に、Token や SourceCode といった ESLint 固有の API について、初心者向けに体系的に説明している記事は少ないのが現状です。

解決策

ESTree:AST ノードの型定義

ESTree は、JavaScript の AST 構造を定義した仕様です。 この仕様に従うことで、異なるパーサー(Espree、Babel、TypeScript ESLint など)が生成する AST が共通のインターフェースを持つようになります。

主要なノードタイプ

ESTree には、以下のような主要なノードタイプが定義されています。

#ノードタイプ説明
1Programプログラム全体のルートノード-
2VariableDeclaration変数宣言const x = 1;
3FunctionDeclaration関数宣言function foo() {}
4Identifier識別子(変数名など)x, foo
5Literalリテラル値"hello", 42, true
6BinaryExpression二項演算a + b
7CallExpression関数呼び出しfoo()
8MemberExpressionメンバーアクセスobj.prop

ノードの共通構造

すべてのノードは、以下の共通プロパティを持っています。

typescript// すべてのノードが持つ基本的なプロパティ
interface BaseNode {
  type: string; // ノードタイプ(例: "Identifier", "Literal")
  loc?: SourceLocation; // ソースコード上の位置情報
  range?: [number, number]; // 開始位置と終了位置(文字インデックス)
}

type プロパティは必須で、ノードの種類を示します。 locrange はオプションですが、ESLint ではこれらを使ってエラーの発生箇所を特定します。

typescript// SourceLocation の構造
interface SourceLocation {
  start: Position; // 開始位置
  end: Position; // 終了位置
}

interface Position {
  line: number; // 行番号(1から始まる)
  column: number; // 列番号(0から始まる)
}

位置情報により、「この問題は何行目の何文字目で発生した」という詳細なレポートが可能になります。

下図は、AST ノードの階層構造を示しています。

mermaidflowchart TB
  program["Program"] -->|body| varDecl["VariableDeclaration"]
  varDecl -->|declarations| varDeclr["VariableDeclarator"]
  varDeclr -->|id| ident["Identifier<br/>(変数名)"]
  varDeclr -->|init| binary["BinaryExpression"]
  binary -->|left| lit1["Literal<br/>(5)"]
  binary -->|operator| op["+"]
  binary -->|right| lit2["Literal<br/>(3)"]

この図は const x = 5 + 3; というコードの AST 構造を表しています。 ルートの Program ノードから始まり、変数宣言、識別子、二項演算といった各要素がツリー構造で表現されていることがわかります。

Token:字句単位の要素

AST ノードが構文構造を表すのに対し、Token は字句単位の要素を表します。 Token は、ソースコードをパースする際に最初に生成される単位で、キーワード、識別子、演算子、括弧、文字列などが該当します。

Token の種類

ESLint では、以下のような Token タイプが定義されています。

#トークンタイプ説明
1KeywordJavaScript のキーワードconst, function, if
2Identifier識別子myVariable, foo
3Punctuator句読点・演算子{, }, +, ;
4String文字列リテラル"hello", 'world'
5Numeric数値リテラル42, 3.14
6Templateテンプレートリテラル`hello ${name}`

Token の構造

各 Token は、以下のような構造を持っています。

typescript// Tokenの基本構造
interface Token {
  type: string; // トークンタイプ
  value: string; // トークンの文字列値
  range: [number, number]; // 開始・終了位置
  loc: SourceLocation; // 行・列の位置情報
}

たとえば、const x = 5; というコードは以下のような Token に分解されます。

typescript// "const x = 5;" のToken例
[
  { type: "Keyword", value: "const", range: [0, 5], loc: {...} },
  { type: "Identifier", value: "x", range: [6, 7], loc: {...} },
  { type: "Punctuator", value: "=", range: [8, 9], loc: {...} },
  { type: "Numeric", value: "5", range: [10, 11], loc: {...} },
  { type: "Punctuator", value: ";", range: [11, 12], loc: {...} }
]

このように、ソースコードが字句単位で分解されることで、「セミコロンの有無」や「演算子の前後のスペース」といった細かいスタイルチェックが可能になります。

SourceCode:ESLint のヘルパーオブジェクト

SourceCode は、ESLint がルール作成者に提供するヘルパーオブジェクトです。 AST ノードや Token に直接アクセスするだけでなく、便利なメソッドを通じてソースコードの情報を取得できます。

SourceCode の主要メソッド

SourceCode オブジェクトには、以下のような便利なメソッドが用意されています。

#メソッド説明戻り値
1getText(node?)ノードまたはソースコード全体のテキストを取得string
2getTokens(node)ノードに含まれるすべての Token を取得Token[]
3getFirstToken(node)ノードの最初の Token を取得Token
4getLastToken(node)ノードの最後の Token を取得Token
5getCommentsBefore(nodeOrToken)指定要素の前のコメントを取得Comment[]
6getCommentsAfter(nodeOrToken)指定要素の後のコメントを取得Comment[]
7getScope(node)ノードのスコープ情報を取得Scope

SourceCode の活用例

以下は、SourceCode を使って関数名を取得する例です。

typescript// SourceCodeを使った関数名の取得
function getFunctionName(node, sourceCode) {
  // FunctionDeclarationノードの場合
  if (node.type === 'FunctionDeclaration') {
    // idプロパティから関数名を取得
    const nameNode = node.id;
    return sourceCode.getText(nameNode);
  }
  return null;
}

getText() メソッドを使うことで、ノードに対応するソースコードの文字列を簡単に取得できます。

次に、Token を使ってセミコロンの有無をチェックする例を見てみましょう。

typescript// Tokenを使ったセミコロンチェック
function hasSemicolon(node, sourceCode) {
  // ノードの最後のTokenを取得
  const lastToken = sourceCode.getLastToken(node);

  // セミコロンかどうかをチェック
  return lastToken && lastToken.value === ';';
}

getLastToken() メソッドにより、ノードの末尾にセミコロンがあるかどうかを判定できます。

さらに、コメントを取得する例も見てみましょう。

typescript// コメントの取得
function hasCommentBefore(node, sourceCode) {
  // ノードの前にあるコメントを取得
  const comments = sourceCode.getCommentsBefore(node);

  // コメントが存在するかチェック
  return comments.length > 0;
}

getCommentsBefore() を使えば、特定のノードの前に書かれたコメントを取得できます。 これは「特定の関数には必ずコメントを書く」といったルールを実装する際に役立ちます。

具体例

実際のコードとその AST 構造

ここでは、具体的な JavaScript コードがどのような AST 構造になるかを見ていきます。

サンプルコード

以下のシンプルな変数宣言を例にします。

javascript// 変数宣言の例
const greeting = 'Hello';

このコードは、定数 greeting を宣言し、文字列 "Hello" を代入しています。

AST 構造の詳細

上記のコードは、以下のような JSON 形式の AST に変換されます。

json{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "greeting"
          },
          "init": {
            "type": "Literal",
            "value": "Hello",
            "raw": "\"Hello\""
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

この JSON 構造を見ると、以下のことがわかります。

  • ルートは Program ノード
  • body 配列に VariableDeclaration が含まれる
  • 変数宣言の kind"const"
  • declarations 配列に VariableDeclarator が含まれる
  • 変数名は Identifier ノードで表現され、name プロパティに "greeting" が格納される
  • 初期値は Literal ノードで、value"Hello" が入る

下図は、このコードの AST 構造を視覚的に表しています。

mermaidflowchart TB
  prog["Program"] --|body&#91;0&#93;|--> vardecl["VariableDeclaration<br />(kind: const)"]
  vardecl --|declarations&#91;0&#93;|--> declr["VariableDeclarator"]
  declr --|id|--> id["Identifier<br />(name: greeting)"]
  declr --|init|--> lit["Literal<br />(value: Hello)"]

このように、シンプルなコードでも複数のノードが階層的に組み合わさって構造が形成されます。

カスタムルールの実装例

実際に ESLint のカスタムルールを作成してみましょう。 ここでは、「console.log を使用禁止にするルール」を例にします。

ルールの骨組み

ESLint のカスタムルールは、以下のような構造で定義します。

javascript// カスタムルールの基本構造
module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'console.logの使用を禁止する',
    },
  },
  create(context) {
    // ルールの実装
    return {};
  },
};

meta オブジェクトにはルールのメタ情報を記述し、create 関数でルールのロジックを実装します。

ノードの監視

console.logCallExpression ノードとして表現されます。 この CallExpression を監視し、calleeconsole.log であるかをチェックします。

javascript// CallExpressionノードを監視
create(context) {
  return {
    CallExpression(node) {
      // このノードが関数呼び出しの場合に実行される
    }
  };
}

create 関数が返すオブジェクトのキーにノードタイプを指定することで、そのノードが検出されたときに関数が実行されます。

console.log の判定

次に、CallExpressionconsole.log であるかを判定します。

javascript// console.logかどうかを判定
create(context) {
  return {
    CallExpression(node) {
      const callee = node.callee;

      // MemberExpression (obj.prop形式) かチェック
      if (callee.type === 'MemberExpression') {
        const object = callee.object;
        const property = callee.property;

        // objectがconsole、propertyがlogかチェック
        if (
          object.type === 'Identifier' &&
          object.name === 'console' &&
          property.type === 'Identifier' &&
          property.name === 'log'
        ) {
          // console.logが見つかった場合の処理
        }
      }
    }
  };
}

calleeMemberExpression の場合、objectproperty を調べて console.log であるかを確認します。

エラーレポート

console.log が見つかったら、context.report() でエラーを報告します。

javascript// エラーを報告
if (
  object.type === 'Identifier' &&
  object.name === 'console' &&
  property.type === 'Identifier' &&
  property.name === 'log'
) {
  context.report({
    node: node,
    message: 'console.logの使用は禁止されています',
  });
}

context.report()nodemessage を渡すことで、ESLint がエラーメッセージを出力します。

完成版のルール

すべてをまとめると、以下のようなカスタムルールが完成します。

javascript// 完成版:console.log禁止ルール
module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'console.logの使用を禁止する',
    },
  },
  create(context) {
    return {
      CallExpression(node) {
        const callee = node.callee;

        if (callee.type === 'MemberExpression') {
          const object = callee.object;
          const property = callee.property;

          if (
            object.type === 'Identifier' &&
            object.name === 'console' &&
            property.type === 'Identifier' &&
            property.name === 'log'
          ) {
            context.report({
              node: node,
              message:
                'console.logの使用は禁止されています',
            });
          }
        }
      },
    };
  },
};

このルールを .eslintrc.js で有効化すれば、コード内の console.log がすべて検出されます。

SourceCode を活用した高度な例

次に、SourceCode オブジェクトを活用したより高度な例を見てみましょう。 「関数の前にコメントがない場合に警告する」というルールを作成します。

ルールの骨組みとノード監視

まず、FunctionDeclaration ノードを監視します。

javascript// 関数宣言を監視
create(context) {
  const sourceCode = context.getSourceCode();

  return {
    FunctionDeclaration(node) {
      // 関数宣言が見つかったときの処理
    }
  };
}

context.getSourceCode() で SourceCode オブジェクトを取得します。

コメントの確認

SourceCode の getCommentsBefore() メソッドを使って、関数の前にコメントがあるかをチェックします。

javascript// コメントの有無をチェック
FunctionDeclaration(node) {
  // 関数の前のコメントを取得
  const comments = sourceCode.getCommentsBefore(node);

  // コメントがない場合
  if (comments.length === 0) {
    context.report({
      node: node,
      message: '関数の前にコメントを記述してください'
    });
  }
}

comments.length0 の場合、コメントが存在しないためエラーを報告します。

完成版のルール

完成版は以下のようになります。

javascript// 完成版:関数コメント必須ルール
module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: '関数の前にコメントを必須にする',
    },
  },
  create(context) {
    const sourceCode = context.getSourceCode();

    return {
      FunctionDeclaration(node) {
        const comments = sourceCode.getCommentsBefore(node);

        if (comments.length === 0) {
          context.report({
            node: node,
            message: '関数の前にコメントを記述してください',
          });
        }
      },
    };
  },
};

このルールにより、コメントのない関数を検出できます。

AST を可視化する便利ツール

AST の構造を理解するには、実際にコードをパースして可視化してみるのが一番です。 以下のようなオンラインツールを使うと、手軽に AST を確認できます。

AST Explorer

AST Explorer は、最も有名な AST 可視化ツールです。 左側にコードを入力すると、右側にリアルタイムで AST が表示されます。

使い方は以下のとおりです。

  1. ブラウザで https://astexplorer.net/ にアクセス
  2. 左上の「Parser」で espree を選択(ESLint と同じパーサー)
  3. 左側のエディタにコードを入力
  4. 右側に AST が JSON 形式で表示される

このツールを使えば、「このコードはどんなノードになるのか」を即座に確認できます。 カスタムルールを作成する際は、まず AST Explorer で対象のコードをパースして、どのノードを監視すべきかを調べると効率的です。

まとめ

本記事では、ESLint のカスタムルール作成に欠かせない、AST、ESTree、Token、SourceCode の基礎を解説しました。 これらの概念を理解することで、ESLint がどのようにコードを解析しているかが明確になったかと思います。

改めて要点をまとめると、以下のようになります。

  • AST(抽象構文木) は、ソースコードを木構造で表現したもので、ESLint の解析の基盤です
  • ESTree は、JavaScript AST の標準仕様で、ノードタイプや構造が明確に定義されています
  • Token は字句単位の要素で、キーワード、識別子、演算子などが含まれます
  • SourceCode は、ESLint が提供するヘルパーオブジェクトで、テキスト取得やコメント取得などの便利なメソッドがあります

カスタムルールを作成する際は、まず対象のコードがどのような AST 構造になるかを AST Explorer で確認し、適切なノードタイプを監視することが重要です。 そして、SourceCode オブジェクトのメソッドを活用することで、より柔軟で強力なルールを実装できます。

今回学んだ知識を活かして、ぜひプロジェクト固有の ESLint カスタムルールを作成してみてください。 コードの品質向上や、チーム内のコーディング規約の統一に大いに役立つはずです。

関連リンク