2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ユニークビジョン株式会社Advent Calendar 2022

Day 23

Typescriptの抽象構文木を眺める

Last updated at Posted at 2022-12-22

抽象構文木とは

  • Abstract Syntax Tree(AST)のこと
  • コードをパースして木構造の集合として扱えるようにしたもの
  • eslintやbabelなどで利用されている

TypescriptにおけるASTの立ち位置

  • Typescriptのコンパイラであるtscは内部でソースコードからASTを構築し、Typescriptを理解している

実際のASTを見てみる

今回は、eslintのカスタムプラグインを自作する準備としてASTに慣れておこう、という目的があるので、typescript-eslintのASTパーサであるtypescript-estreeを使ってASTを抽出します。
tscが実際に用いるASTはTypescript Complier APIで出力することができるので、興味のある方はComplierAPIのリポジトリをご覧ください。

import { parse } from '@typescript-eslint/typescript-estree';
import { readFile } from './file_utils';


// 文字列としてtypescriptのソースコードを読みASTにパースする
const sampleCode = readFile('./src/sample.ts')
if (sampleCode) {
const ast = parse(sampleCode, {
  loc: false,
  range: false,
});

  console.log(JSON.stringify(ast, null, 2))
}

Sample1

最も単純なコード

ソースコード

console.log('Hello World!')

AST

{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "MemberExpression",
          "object": {
            "type": "Identifier",
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "name": "log"
          },
          "computed": false,
          "optional": false
        },
        "arguments": [
          {
            "type": "Literal",
            "value": "Hello World!",
            "raw": "'Hello World!'"
          }
        ],
        "optional": false
      }
    }
  ],
  "sourceType": "script"
}

consoleというMemberExpressionのlogというpropertyがHello World!というLiteralのargumentsを渡されて評価された。ということがそのままJSON形式で表現されている。

Sample2

変数定義

const hoge = 'hoge'

let fuga = 'fuga'

var piyo = 'piyo'

AST

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "hoge"
          },
          "init": {
            "type": "Literal",
            "value": "hoge",
            "raw": "'hoge'"
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "fuga"
          },
          "init": {
            "type": "Literal",
            "value": "fuga",
            "raw": "'fuga'"
          }
        }
      ],
      "kind": "let"
    },
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "piyo"
          },
          "init": {
            "type": "Literal",
            "value": "piyo",
            "raw": "'piyo'"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}

変数定義の違いはkindで表現され、それぞれがどのようなステートメントで定義されたかが表現されている。

Sample3

class定義を伴うよくあるソースコード

class Hoge {
  name!: string
  age!: number

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

class Fuga extends Hoge {
  constructor(hoge: Hoge) {
    super(hoge.name, hoge.age)
  }

  greeting() {
    return `I'm ${this.name}.`
  }
}

const fuga = new Fuga({ name: 'fuga', age: 12 })


console.log(fuga.greeting())

AST

{
  "type": "Program",
  "body": [
    {
      "type": "ClassDeclaration",
      "id": {
        "type": "Identifier",
        "name": "Hoge"
      },
      "body": {
        "type": "ClassBody",
        "body": [
          {
            "type": "PropertyDefinition",
            "key": {
              "type": "Identifier",
              "name": "name"
            },
            "value": null,
            "computed": false,
            "static": false,
            "declare": false,
            "override": false,
            "typeAnnotation": {
              "type": "TSTypeAnnotation",
              "typeAnnotation": {
                "type": "TSStringKeyword"
              }
            },
            "definite": true
          },
          {
            "type": "PropertyDefinition",
            "key": {
              "type": "Identifier",
              "name": "age"
            },
            "value": null,
            "computed": false,
            "static": false,
            "declare": false,
            "override": false,
            "typeAnnotation": {
              "type": "TSTypeAnnotation",
              "typeAnnotation": {
                "type": "TSNumberKeyword"
              }
            }
          },
          {
            "type": "MethodDefinition",
            "key": {
              "type": "Identifier",
              "name": "constructor"
            },
            "value": {
              "type": "FunctionExpression",
              "id": null,
              "params": [
                {
                  "type": "Identifier",
                  "name": "name",
                  "typeAnnotation": {
                    "type": "TSTypeAnnotation",
                    "typeAnnotation": {
                      "type": "TSStringKeyword"
                    }
                  }
                },
                {
                  "type": "Identifier",
                  "name": "age",
                  "typeAnnotation": {
                    "type": "TSTypeAnnotation",
                    "typeAnnotation": {
                      "type": "TSNumberKeyword"
                    }
                  }
                }
              ],
              "generator": false,
              "expression": false,
              "async": false,
              "body": {
                "type": "BlockStatement",
                "body": [
                  {
                    "type": "ExpressionStatement",
                    "expression": {
                      "type": "AssignmentExpression",
                      "operator": "=",
                      "left": {
                        "type": "MemberExpression",
                        "object": {
                          "type": "ThisExpression"
                        },
                        "property": {
                          "type": "Identifier",
                          "name": "name"
                        },
                        "computed": false,
                        "optional": false
                      },
                      "right": {
                        "type": "Identifier",
                        "name": "name"
                      }
                    }
                  },
                  {
                    "type": "ExpressionStatement",
                    "expression": {
                      "type": "AssignmentExpression",
                      "operator": "=",
                      "left": {
                        "type": "MemberExpression",
                        "object": {
                          "type": "ThisExpression"
                        },
                        "property": {
                          "type": "Identifier",
                          "name": "age"
                        },
                        "computed": false,
                        "optional": false
                      },
                      "right": {
                        "type": "Identifier",
                        "name": "age"
                      }
                    }
                  }
                ]
              }
            },
            "computed": false,
            "static": false,
            "kind": "constructor",
            "override": false
          }
        ]
      },
      "superClass": null
    },
    {
      "type": "ClassDeclaration",
      "id": {
        "type": "Identifier",
        "name": "Fuga"
      },
      "body": {
        "type": "ClassBody",
        "body": [
          {
            "type": "MethodDefinition",
            "key": {
              "type": "Identifier",
              "name": "constructor"
            },
            "value": {
              "type": "FunctionExpression",
              "id": null,
              "params": [
                {
                  "type": "Identifier",
                  "name": "hoge",
                  "typeAnnotation": {
                    "type": "TSTypeAnnotation",
                    "typeAnnotation": {
                      "type": "TSTypeReference",
                      "typeName": {
                        "type": "Identifier",
                        "name": "Hoge"
                      }
                    }
                  }
                }
              ],
              "generator": false,
              "expression": false,
              "async": false,
              "body": {
                "type": "BlockStatement",
                "body": [
                  {
                    "type": "ExpressionStatement",
                    "expression": {
                      "type": "CallExpression",
                      "callee": {
                        "type": "Super"
                      },
                      "arguments": [
                        {
                          "type": "MemberExpression",
                          "object": {
                            "type": "Identifier",
                            "name": "hoge"
                          },
                          "property": {
                            "type": "Identifier",
                            "name": "name"
                          },
                          "computed": false,
                          "optional": false
                        },
                        {
                          "type": "MemberExpression",
                          "object": {
                            "type": "Identifier",
                            "name": "hoge"
                          },
                          "property": {
                            "type": "Identifier",
                            "name": "age"
                          },
                          "computed": false,
                          "optional": false
                        }
                      ],
                      "optional": false
                    }
                  }
                ]
              }
            },
            "computed": false,
            "static": false,
            "kind": "constructor",
            "override": false
          },
          {
            "type": "MethodDefinition",
            "key": {
              "type": "Identifier",
              "name": "greeting"
            },
            "value": {
              "type": "FunctionExpression",
              "id": null,
              "generator": false,
              "expression": false,
              "async": false,
              "body": {
                "type": "BlockStatement",
                "body": [
                  {
                    "type": "ReturnStatement",
                    "argument": {
                      "type": "TemplateLiteral",
                      "quasis": [
                        {
                          "type": "TemplateElement",
                          "value": {
                            "raw": "I'm ",
                            "cooked": "I'm "
                          },
                          "tail": false
                        },
                        {
                          "type": "TemplateElement",
                          "value": {
                            "raw": ".",
                            "cooked": "."
                          },
                          "tail": true
                        }
                      ],
                      "expressions": [
                        {
                          "type": "MemberExpression",
                          "object": {
                            "type": "ThisExpression"
                          },
                          "property": {
                            "type": "Identifier",
                            "name": "name"
                          },
                          "computed": false,
                          "optional": false
                        }
                      ]
                    }
                  }
                ]
              },
              "params": []
            },
            "computed": false,
            "static": false,
            "kind": "method",
            "override": false
          }
        ]
      },
      "superClass": {
        "type": "Identifier",
        "name": "Hoge"
      }
    },
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "fuga"
          },
          "init": {
            "type": "NewExpression",
            "callee": {
              "type": "Identifier",
              "name": "Fuga"
            },
            "arguments": [
              {
                "type": "ObjectExpression",
                "properties": [
                  {
                    "type": "Property",
                    "key": {
                      "type": "Identifier",
                      "name": "name"
                    },
                    "value": {
                      "type": "Literal",
                      "value": "fuga",
                      "raw": "'fuga'"
                    },
                    "computed": false,
                    "method": false,
                    "shorthand": false,
                    "kind": "init"
                  },
                  {
                    "type": "Property",
                    "key": {
                      "type": "Identifier",
                      "name": "age"
                    },
                    "value": {
                      "type": "Literal",
                      "value": 12,
                      "raw": "12"
                    },
                    "computed": false,
                    "method": false,
                    "shorthand": false,
                    "kind": "init"
                  }
                ]
              }
            ]
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "MemberExpression",
          "object": {
            "type": "Identifier",
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "name": "log"
          },
          "computed": false,
          "optional": false
        },
        "arguments": [
          {
            "type": "CallExpression",
            "callee": {
              "type": "MemberExpression",
              "object": {
                "type": "Identifier",
                "name": "fuga"
              },
              "property": {
                "type": "Identifier",
                "name": "greeting"
              },
              "computed": false,
              "optional": false
            },
            "arguments": [],
            "optional": false
          }
        ],
        "optional": false
      }
    }
  ],
  "sourceType": "script"
}

まず初めに、Hogeというクラスが定義されている。二つのメンバー変数nameとageの違いは、変数名であるPropertyDefinition.key.nameと型情報であるtypeAnnotationがTSStringKeywordとTSNumberKeywordである点。それから、definiteが付いているかどうかという点。
次に、Fugaというクラス定義を見てみると、constructorの引数のtypeAnnotationがHogeになっている。ユーザー定義型であってもしっかりと型情報が載ってきている。
また、superClassがHogeであるという情報も載ってきていて、継承関係も表現できている。

Sample4

typescript-eslint/no-use-before-defineで警告が出るパターン

console.log(hoge)

var hoge = 'hoge'

AST

{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "MemberExpression",
          "object": {
            "type": "Identifier",
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "name": "log"
          },
          "computed": false,
          "optional": false
        },
        "arguments": [
          {
            "type": "Identifier",
            "name": "hoge"
          }
        ],
        "optional": false
      }
    },
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "hoge"
          },
          "init": {
            "type": "Literal",
            "value": "hoge",
            "raw": "'hoge'"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}

console.logのargumentsが、Sample1のASTとは異なり、"type": "Literal"ではなく、"type": "Identifier"となっているため、これはユーザー定義の変数であることがわかる。ASTのbodyは配列になっていて、その順番はソースコードの記載順序に一致しているため、"name": "hoge"を持った何らかの変数がconsole.logの呼び出しよりも若いインデックスで出てこなければ、定義されていない変数を呼び出しているということがわかる。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?