Open5

BabelとAST

Koki NagaiKoki Nagai

BabelのAST変換やtraverseについてぼんやりと全体感しか掴めていないので、詳しくどんな挙動になっているか調べる

Koki NagaiKoki Nagai

その前にBabelとは、JavaScriptのトランスパイラで、主に古いブラウザに下位互換性のあるJavaScriptへ変換する処理を行う。

// 変換前
[1, 2, 3].map(n => n + 1);

// ES5へ変換後
[1, 2, 3].map(function(n) {
  return n + 1;
});
Koki NagaiKoki Nagai

このサイトで、JavaScriptのコードをASTに変換した場合の結果が見れる。

例えば、

const hoge = () => 1 + 1

をASTに変換すると、以下のようなASTに変換される。

{
  "type": "File",
  "start": 0,
  "end": 29,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 1,
      "column": 29
    }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 29,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 1,
        "column": 29
      }
    },
    "sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 0,
        "end": 29,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 1,
            "column": 29
          }
        },
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 6,
            "end": 28,
            "loc": {
              "start": {
                "line": 1,
                "column": 6
              },
              "end": {
                "line": 1,
                "column": 28
              }
            },
            "id": {
              "type": "Identifier",
              "start": 6,
              "end": 10,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 6
                },
                "end": {
                  "line": 1,
                  "column": 10
                },
                "identifierName": "hoge"
              },
              "name": "hoge"
            },
            "init": {
              "type": "ArrowFunctionExpression",
              "start": 13,
              "end": 28,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 13
                },
                "end": {
                  "line": 1,
                  "column": 28
                }
              },
              "id": null,
              "generator": false,
              "async": false,
              "params": [],
              "body": {
                "type": "BinaryExpression",
                "start": 19,
                "end": 28,
                "loc": {
                  "start": {
                    "line": 1,
                    "column": 19
                  },
                  "end": {
                    "line": 1,
                    "column": 28
                  }
                },
                "left": {
                  "type": "NumericLiteral",
                  "start": 19,
                  "end": 22,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 19
                    },
                    "end": {
                      "line": 1,
                      "column": 22
                    }
                  },
                  "extra": {
                    "rawValue": 200,
                    "raw": "200"
                  },
                  "value": 200
                },
                "operator": "+",
                "right": {
                  "type": "NumericLiteral",
                  "start": 25,
                  "end": 28,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 25
                    },
                    "end": {
                      "line": 1,
                      "column": 28
                    }
                  },
                  "extra": {
                    "rawValue": 300,
                    "raw": "300"
                  },
                  "value": 300
                }
              }
            }
          }
        ],
        "kind": "const"
      }
    ],
    "directives": []
  },
  "comments": []
}

ASTを見てみると、各階層の構造が

{
  "type": "Program",
    "start": 0,
    "end": 29,
}

のようなオブジェクトで、これらはNodeと呼ばれる。このNodeは、node_modules/@babel/types/lib/index.d.tsで型を見ると、

declare type Node = AnyTypeAnnotation | ArgumentPlaceholder | ArrayExpression | ArrayPattern | ArrayTypeAnnotation | ArrowFunctionExpression | AssignmentExpression | AssignmentPattern | AwaitExpression | BigIntLiteral | Binary | BinaryExpression | BindExpression | Block | BlockParent | BlockStatement | BooleanLiteral | BooleanLiteralTypeAnnotation | BooleanTypeAnnotation | BreakStatement | CallExpression | CatchClause | Class | ClassBody | ClassDeclaration | ClassExpression | ClassImplements | ClassMethod | ClassPrivateMethod | ClassPrivateProperty | ClassProperty | CompletionStatement | Conditional | ConditionalExpression | ContinueStatement | DebuggerStatement | DecimalLiteral | Declaration | DeclareClass | DeclareExportAllDeclaration | DeclareExportDeclaration | DeclareFunction | DeclareInterface | DeclareModule | DeclareModuleExports | DeclareOpaqueType | DeclareTypeAlias | DeclareVariable | DeclaredPredicate | Decorator | Directive | DirectiveLiteral | DoExpression | DoWhileStatement | EmptyStatement | EmptyTypeAnnotation | EnumBody | EnumBooleanBody | EnumBooleanMember | EnumDeclaration | EnumDefaultedMember | EnumMember | EnumNumberBody | EnumNumberMember | EnumStringBody | EnumStringMember | EnumSymbolBody | ExistsTypeAnnotation | ExportAllDeclaration | ExportDeclaration | ExportDefaultDeclaration | ExportDefaultSpecifier | ExportNamedDeclaration | ExportNamespaceSpecifier | ExportSpecifier | Expression | ExpressionStatement | ExpressionWrapper | File | Flow | FlowBaseAnnotation | FlowDeclaration | FlowPredicate | FlowType | For | ForInStatement | ForOfStatement | ForStatement | ForXStatement | Function | FunctionDeclaration | FunctionExpression | FunctionParent | FunctionTypeAnnotation | FunctionTypeParam | GenericTypeAnnotation | Identifier | IfStatement | Immutable | Import | ImportAttribute | ImportDeclaration | ImportDefaultSpecifier | ImportNamespaceSpecifier | ImportSpecifier | IndexedAccessType | InferredPredicate | InterfaceDeclaration | InterfaceExtends | InterfaceTypeAnnotation | InterpreterDirective | IntersectionTypeAnnotation | JSX | JSXAttribute | JSXClosingElement | JSXClosingFragment | JSXElement | JSXEmptyExpression | JSXExpressionContainer | JSXFragment | JSXIdentifier | JSXMemberExpression | JSXNamespacedName | JSXOpeningElement | JSXOpeningFragment | JSXSpreadAttribute | JSXSpreadChild | JSXText | LVal | LabeledStatement | Literal | LogicalExpression | Loop | MemberExpression | MetaProperty | Method | MixedTypeAnnotation | ModuleDeclaration | ModuleExpression | ModuleSpecifier | NewExpression | Noop | NullLiteral | NullLiteralTypeAnnotation | NullableTypeAnnotation | NumberLiteral$1 | NumberLiteralTypeAnnotation | NumberTypeAnnotation | NumericLiteral | ObjectExpression | ObjectMember | ObjectMethod | ObjectPattern | ObjectProperty | ObjectTypeAnnotation | ObjectTypeCallProperty | ObjectTypeIndexer | ObjectTypeInternalSlot | ObjectTypeProperty | ObjectTypeSpreadProperty | OpaqueType | OptionalCallExpression | OptionalIndexedAccessType | OptionalMemberExpression | ParenthesizedExpression | Pattern | PatternLike | PipelineBareFunction | PipelinePrimaryTopicReference | PipelineTopicExpression | Placeholder | Private | PrivateName | Program | Property | Pureish | QualifiedTypeIdentifier | RecordExpression | RegExpLiteral | RegexLiteral$1 | RestElement | RestProperty$1 | ReturnStatement | Scopable | SequenceExpression | SpreadElement | SpreadProperty$1 | Statement | StaticBlock | StringLiteral | StringLiteralTypeAnnotation | StringTypeAnnotation | Super | SwitchCase | SwitchStatement | SymbolTypeAnnotation | TSAnyKeyword | TSArrayType | TSAsExpression | TSBaseType | TSBigIntKeyword | TSBooleanKeyword | TSCallSignatureDeclaration | TSConditionalType | TSConstructSignatureDeclaration | TSConstructorType | TSDeclareFunction | TSDeclareMethod | TSEntityName | TSEnumDeclaration | TSEnumMember | TSExportAssignment | TSExpressionWithTypeArguments | TSExternalModuleReference | TSFunctionType | TSImportEqualsDeclaration | TSImportType | TSIndexSignature | TSIndexedAccessType | TSInferType | TSInterfaceBody | TSInterfaceDeclaration | TSIntersectionType | TSIntrinsicKeyword | TSLiteralType | TSMappedType | TSMethodSignature | TSModuleBlock | TSModuleDeclaration | TSNamedTupleMember | TSNamespaceExportDeclaration | TSNeverKeyword | TSNonNullExpression | TSNullKeyword | TSNumberKeyword | TSObjectKeyword | TSOptionalType | TSParameterProperty | TSParenthesizedType | TSPropertySignature | TSQualifiedName | TSRestType | TSStringKeyword | TSSymbolKeyword | TSThisType | TSTupleType | TSType | TSTypeAliasDeclaration | TSTypeAnnotation | TSTypeAssertion | TSTypeElement | TSTypeLiteral | TSTypeOperator | TSTypeParameter | TSTypeParameterDeclaration | TSTypeParameterInstantiation | TSTypePredicate | TSTypeQuery | TSTypeReference | TSUndefinedKeyword | TSUnionType | TSUnknownKeyword | TSVoidKeyword | TaggedTemplateExpression | TemplateElement | TemplateLiteral | Terminatorless | ThisExpression | ThisTypeAnnotation | ThrowStatement | TopicReference | TryStatement | TupleExpression | TupleTypeAnnotation | TypeAlias | TypeAnnotation | TypeCastExpression | TypeParameter | TypeParameterDeclaration | TypeParameterInstantiation | TypeofTypeAnnotation | UnaryExpression | UnaryLike | UnionTypeAnnotation | UpdateExpression | UserWhitespacable | V8IntrinsicIdentifier | VariableDeclaration | VariableDeclarator | Variance | VoidTypeAnnotation | While | WhileStatement | WithStatement | YieldExpression;

interface BaseNode {
    leadingComments: ReadonlyArray<Comment> | null;
    innerComments: ReadonlyArray<Comment> | null;
    trailingComments: ReadonlyArray<Comment> | null;
    start: number | null;
    end: number | null;
    loc: SourceLocation | null;
    type: Node["type"];
    range?: [number, number];
    extra?: Record<string, unknown>;
}

interface AnyTypeAnnotation extends BaseNode {
    type: "AnyTypeAnnotation";
}

// ...

のように、様々な種類のNodeの型があることが分かる。また、startやend, locによって元のソースコードのどこに位置していたかを見ることができる。

Koki NagaiKoki Nagai

babelのステージとしては、parse, traverse, generateがあり、

JavaScript -> parse -> AST -> traverse -> AST -> generate -> JavaScript

という流れでトランスパイルされる。

parseは字句解析と構文解析処理の2つに分類され、前者の方はJavaScriptのコードをトークンに分類し、後者は分類されたトークンを変換しやすいASTの形に変換する役割を持つ。

traverseはASTのノードを走査して、ノードの作成、変換、削除の処理を行う。各babelのプラグインはここの変換処理を行っている。

generateは、parseやtraverseで生成したASTを再びJavaScriptのコードへ変換する処理を行う。ここでは深さ優先探索のアルゴリズムが使われる。

Koki NagaiKoki Nagai

実際にASTに変換してJavaScriptのコードを書き換えてみる。

const hoge = 100 + 100const koki = 100 + 100に変換してみようと思う。

まずは、第一フェーズのJavaScriptのコードをparseしてASTに変換する処理を行う。parseには@babel/parserparseを使用する。

target.js
const hoge = 100 + 100;
index.js
const { parse } = require("@babel/parser");
const fs = require("fs");

const code = fs.readFileSync(__dirname + "/target.js", "utf-8");
const ast = parse(code, {});

fs.writeFileSync(__dirname + "/result.json", JSON.stringify(ast));

target.jsconst hoge = 100 + 100をASTに変換したJSONをresult.jsonへ書き込んでいる。
で、そのJSONがこちら。

{
  "type": "File",
  "start": 0,
  "end": 24,
  "loc": {
    "start": { "line": 1, "column": 0 },
    "end": { "line": 2, "column": 0 }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 24,
    "loc": {
      "start": { "line": 1, "column": 0 },
      "end": { "line": 2, "column": 0 }
    },
    "sourceType": "script",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 0,
        "end": 23,
        "loc": {
          "start": { "line": 1, "column": 0 },
          "end": { "line": 1, "column": 23 }
        },
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 6,
            "end": 22,
            "loc": {
              "start": { "line": 1, "column": 6 },
              "end": { "line": 1, "column": 22 }
            },
            "id": {
              "type": "Identifier",
              "start": 6,
              "end": 10,
              "loc": {
                "start": { "line": 1, "column": 6 },
                "end": { "line": 1, "column": 10 },
                "identifierName": "hoge"
              },
              "name": "hoge"
            },
            "init": {
              "type": "BinaryExpression",
              "start": 13,
              "end": 22,
              "loc": {
                "start": { "line": 1, "column": 13 },
                "end": { "line": 1, "column": 22 }
              },
              "left": {
                "type": "NumericLiteral",
                "start": 13,
                "end": 16,
                "loc": {
                  "start": { "line": 1, "column": 13 },
                  "end": { "line": 1, "column": 16 }
                },
                "extra": { "rawValue": 100, "raw": "100" },
                "value": 100
              },
              "operator": "+",
              "right": {
                "type": "NumericLiteral",
                "start": 19,
                "end": 22,
                "loc": {
                  "start": { "line": 1, "column": 19 },
                  "end": { "line": 1, "column": 22 }
                },
                "extra": { "rawValue": 100, "raw": "100" },
                "value": 100
              }
            }
          }
        ],
        "kind": "const"
      }
    ],
    "directives": []
  },
  "comments": []
}

たった一行でこれだけのJSONが吐き出される。Identifierを見ると、hogeが記述されているので、変換する対象はここが怪しそう。

次に、traverseしてAST上でhogeからkokiに変換する処理を加える。

const { parse } = require("@babel/parser");
const { default: traverse } = require("@babel/traverse");
const fs = require("fs");

const code = fs.readFileSync(__dirname + "/target.js", "utf-8");
const ast = parse(code, {});

traverse(ast, {
  enter(path) {
    if (path.isIdentifier({ name: "hoge" })) {
      path.node.name = "koki";
    }
  },
});

fs.writeFileSync(__dirname + "/result.json", JSON.stringify(ast));

ASTの中でIdentifierがあるが、if (path.isIdentifier({ name: "hoge" }))の箇所でhogeがあればkokiに変換している。ちなみにenterは各Node(typeがNumericLiteralやNumericLiteralのオブジェクト)が入るタイミングで呼ばれ、pathにはNode型を拡張したNodePath型に当たるものが入る。
NodePath型を見ると、

export class NodePath<T = Node> {
    constructor(hub: Hub, parent: Node);
    parent: Node;
    hub: Hub;
    contexts: TraversalContext[];
    data: object;
    shouldSkip: boolean;
    shouldStop: boolean;
    removed: boolean;
    state: any;
    opts: object;
    skipKeys: object;
    parentPath: T extends t.Program ? null : NodePath;
    context: TraversalContext;
    container: object | object[];
    listKey: string;
    inList: boolean;
    // ...

と、とても長い型が定義されていることがわかる。

で次に、実際にトラバースしたASTは以下の通り。

{
  "type": "File",
  "start": 0,
  "end": 24,
  "loc": {
    "start": { "line": 1, "column": 0 },
    "end": { "line": 2, "column": 0 }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 24,
    "loc": {
      "start": { "line": 1, "column": 0 },
      "end": { "line": 2, "column": 0 }
    },
    "sourceType": "script",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 0,
        "end": 23,
        "loc": {
          "start": { "line": 1, "column": 0 },
          "end": { "line": 1, "column": 23 }
        },
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 6,
            "end": 22,
            "loc": {
              "start": { "line": 1, "column": 6 },
              "end": { "line": 1, "column": 22 }
            },
            "id": {
              "type": "Identifier",
              "start": 6,
              "end": 10,
              "loc": {
                "start": { "line": 1, "column": 6 },
                "end": { "line": 1, "column": 10 },
                "identifierName": "hoge"
              },
              "name": "koki" // 👈  変換されている
            },
            "init": {
              "type": "BinaryExpression",
              "start": 13,
              "end": 22,
              "loc": {
                "start": { "line": 1, "column": 13 },
                "end": { "line": 1, "column": 22 }
              },
              "left": {
                "type": "NumericLiteral",
                "start": 13,
                "end": 16,
                "loc": {
                  "start": { "line": 1, "column": 13 },
                  "end": { "line": 1, "column": 16 }
                },
                "extra": { "rawValue": 100, "raw": "100" },
                "value": 100
              },
              "operator": "+",
              "right": {
                "type": "NumericLiteral",
                "start": 19,
                "end": 22,
                "loc": {
                  "start": { "line": 1, "column": 19 },
                  "end": { "line": 1, "column": 22 }
                },
                "extra": { "rawValue": 100, "raw": "100" },
                "value": 100
              }
            }
          }
        ],
        "kind": "const"
      }
    ],
    "directives": []
  },
  "comments": []
}

ASTを見ても、hogeからkokiに変換されていることが分かる。

最終的にAST -> JavaScriptに変換する処理は@babel/generatorのgenerate関数を使う。

const { parse } = require("@babel/parser");
const { default: traverse } = require("@babel/traverse");
const { default: generate } = require("@babel/generator");
const fs = require("fs");

const code = fs.readFileSync(__dirname + "/code.js", "utf-8");
const ast = parse(code, {});

traverse(ast, {
  enter(path) {
    if (path.isIdentifier({ name: "hoge" })) {
      path.node.name = "koki";
    }
  },
});

const newCode = generate(ast);
fs.writeFileSync(__dirname + "/result.js", newCode.code);
const koki = 100 + 100;

ASTをgenerateの引数に渡すと、GeneratorResultという型のオブジェクトが返却される。GeneratorResultは、

export interface GeneratorResult {
    code: string;
    map: {
        version: number;
        sources: string[];
        names: string[];
        sourceRoot?: string | undefined;
        sourcesContent?: string[] | undefined;
        mappings: string;
        file: string;
    } | null;
}

という型で、codeに変換したJavaScriptのコードが入ってくる。