抽象構文木とは
- 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の呼び出しよりも若いインデックスで出てこなければ、定義されていない変数を呼び出しているということがわかる。