BabelとAST
BabelのAST変換やtraverseについてぼんやりと全体感しか掴めていないので、詳しくどんな挙動になっているか調べる
その前にBabelとは、JavaScriptのトランスパイラで、主に古いブラウザに下位互換性のあるJavaScriptへ変換する処理を行う。
// 変換前
[1, 2, 3].map(n => n + 1);
// ES5へ変換後
[1, 2, 3].map(function(n) {
return n + 1;
});
このサイトで、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によって元のソースコードのどこに位置していたかを見ることができる。
babelのステージとしては、parse
, traverse
, generate
があり、
JavaScript -> parse -> AST -> traverse -> AST -> generate -> JavaScript
という流れでトランスパイルされる。
parseは字句解析と構文解析処理の2つに分類され、前者の方はJavaScriptのコードをトークンに分類し、後者は分類されたトークンを変換しやすいASTの形に変換する役割を持つ。
traverseはASTのノードを走査して、ノードの作成、変換、削除の処理を行う。各babelのプラグインはここの変換処理を行っている。
generateは、parseやtraverseで生成したASTを再びJavaScriptのコードへ変換する処理を行う。ここでは深さ優先探索のアルゴリズムが使われる。
実際にASTに変換してJavaScriptのコードを書き換えてみる。
const hoge = 100 + 100
をconst koki = 100 + 100
に変換してみようと思う。
まずは、第一フェーズのJavaScriptのコードをparseしてASTに変換する処理を行う。parseには@babel/parser
のparse
を使用する。
const hoge = 100 + 100;
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.js
のconst 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のコードが入ってくる。