45.7K Views
May 11, 24
スライド概要
TSKaigi 2024 11:30~12:00 トラック2 セッション
https://tskaigi.org/talks/Himenon
TypeScriptのAPIにはAbstract Syntax Tree(抽象構文木。以降ASTと省略)に関するAPIがあります。抽象構文木は静的解析やSyntax Highlight、Code Generatorなど普段我々が利用しているツールの内部で利用されています。
本発表では、TypeScriptのASTに入門しつつ、その応用であるコードの自動生成をどうやって実現していくか、OpenAPI Code Generatorのライブラリを4年間維持し続けている経験から紹介していきます。
TypeScript ASTを利用した コードジェネレーター 実装入門 Track 2 11:30 ~ 12:00 @Himenon
自己紹介 SNS ● ● GitHub: Himenon Twitter: himenoglyph 所属 ● 株式会社ハイヤールー(Coding Interview platform) エンジニアリング領域 ● ● Webフロントエンド(メイン) 開発支援ツールを作る が趣味 代表的なOSS ● Himenon/openapi-typescript-code-generator ← こ 知見が本発表 ベース!
本発表で利用するサンプルコード github.com/Himenon/tskaigi-2024-code-sample
目次 1. TypeScript AST入門 12分 2. コードジェネレーター実装入門 15分 3. コードジェネレーター 実装ルーチン 3分
目次 1. TypeScript AST入門 12分 2. コードジェネレーター実装入門 15分 3. コードジェネレーター 実装ルーチン 3分
1. TypeScript Abstract Syntax Tree入門
TypeScript AST入門 TypeScript Abstract Syntax Tree(抽象構文木)と ? 一言で説明すると、 ソースコードをプログラマブルにしたデータ構造
TypeScript AST入門 AST(Abstract Syntax Tree)と他 TypeScript APIと 関係性 Compiler API 変換・ Type Check Language Server protocol コード補完・構 文チェック 変換後 コード 利用 Source Code (Plain Text) 解析 Parse AST 利用 TypeScript 操作をしようとすると最初に出てくる基本知識 IDE/エディタ
TypeScript AST入門 TypeScript AST Viewerを使ってASTを見てみる ブラウザでASTを確認するため https://ts-ast-viewer.com ツール
TypeScript AST入門 “Hello World”という文字列を解析する
TypeScript AST入門 “Hello World”という文字列を解析する 1/2 ①Source Codeを入力 ② Source CodeをPrase結果 Tree Viewer Source Text 抽象構文木 ④AST Factory Code 生成コード ③Property Viewer 選択した抽象木 Node Property
TypeScript AST入門 “Hello World” AST “Hello World”; パースされた AST SourceFile ● 文法 / 種別 ファイル ExpressionStatement ○ StringLiteral 式文 文字列 EndofFileToken ファイル 終了識別子
TypeScript AST入門 “Hello” + “World” AST “Hello”; SourceFile “World”; ● ● ExpressionStatement ○ StringLiteral ExpressionStatement ○ StringLiteral EndofFileToken “Hello”に対して “World”に対して
TypeScript AST入門 少し複雑な実装 const mainTask = () => { const subTask = () => {} return subTask(); AST SourceFile ● } mainTask(); ● VariableSatement ○ VariableDeclaration ○ ArrowFunction ■ Block ● VariableStatement ■ ReturnStatement ● CallExpression ExpressionStatement ○ CallExpression
TypeScript AST入門 ASTを可視化すると const mainTask = () => { SourceFile const subTask = () => {} return subTask(); } VariableStatement const mainTask = mainTask(); VariableDeclaration const subTask = ArrowFunction () => { … } EqualsGreaterThanToken Block ExpressionStatement mainTask(); ArrowFucntion
TypeScript AST入門 Abstract Syntax Tree(抽象構文木)と周辺知識 ● ソースコードが文法や抽象化される ○ ● 式、文、ブロックなど普段見えない文法構造がデータとして扱える データ構造として 木構造を取っている root 木構造を扱うアルゴリズムが ASTを取り扱う文脈で出てくる用語 ● ● ● ● root node 幅優先探索(BFS) / 深さ優先探索( DFS) Visitorパターン node
TypeScript AST入門 木構造 取り扱い - 走査(traverse) 走査 各nodeを順番に訪問(visit)すること 1 訪問したnodeに対して変換処理をすること ができる 2 3 4 6 5 7 8
traverse
実装例
import ts from "typescript";
const transformer: ts.TransformerFactory<ts.Node> =
(context) => (rootNode: ts.Node) => {
const visit = (node: ts.Node): ts.Node => {
変数宣言を発見し次第、 newNameに書き換え
if (ts.isVariableDeclaration(node)) {
const newName = ts.factory.createIdentifier("TSKaigi");
return ts.factory.updateVariableDeclaration(node, newName, node.exclamationToken, node.type,
node.initializer);
}
return ts.visitEachChild(node, visit, context);
};
return ts.visitNode(rootNode, visit);
};
ts.transform(source, [transformer]);
visitした先
node
指定したnodeに訪問する
子
nodeもvisitしていく
TypeScript AST入門 ASTを扱う工程 3ステップ Parse Source Codeを ASTに変換 Traverse 木 走査をしながら Nodeを変換 unparse ASTから Source Codeに変換
目次 1. TypeScript AST入門 12分 2. コードジェネレーター実装入門 15分 3. コードジェネレーター 実装ルーチン 3分
2. コードジェネレーター実装入門
コードジェネレーター実装入門 こ 章 次 内容を含みます 1. Code Generator 解決する課題とそ 基本的な構造 2. Code Generator 実装方法 3. 経験則から得られたアンチパターンとそ 対策
2.1 コードジェネレーター実装入門 Code Generator Code Generator ● ● 前後で2つ 解決する課題とそ 基本的な構造 事象が顕になる 出力側 ○ 成果物 TypeScriptで固定される ○ 生成されるコード 品質が均一になる 入力側 ○ 大抵 ケースで仕様が決まった入力をCode Generatorに渡せ ? よい(Code Generator CODE GENERATOR 入力 仕様やSchema 出力 コード生成機 ※AST 利用 有無 ここで 都合を気にしないでよい) ? 成果物 関係ない ソースコードなど
2.1 コードジェネレーター実装入門 Code Generator Code Generator ● ● 前後で2つ 解決する課題とそ 基本的な構造 事象が顕になる 出力側 ○ 成果物 TypeScriptで固定される ○ 生成されるコード 品質が均一になる 入力側 ○ 大抵 ケースで仕様が決まった入力をCode Generatorに渡せ よい(Code Generator 都合を気にしないでよい) 依存方向 ? CODE GENERATOR 入力 仕様やSchema 出力 コード生成機 ※AST 利用 有無 ここで ? 成果物 関係ない ソースコードなど
2.1 コードジェネレーター実装入門 Code Generator Code Generator ● ● 前後で2つ 解決する課題とそ 基本的な構造 事象が顕になる 出力側 ○ 成果物 TypeScriptで固定される ○ 生成されるコード 品質が均一になる 入力側 ○ 大抵 ケースで仕様が決まった入力をCode Generatorに渡せ よい(Code Generator 開発者が少ない 都合を気にしないでよい) 無数 選択肢が生まれる 依存方向 ? CODE GENERATOR 入力 仕様やSchema 出力 コード生成機 ※AST 利用 有無 ここで ? 成果物 関係ない ソースコードなど
2.1 コードジェネレーター実装入門 Code Generator Code Generator 解決する課題とそ 基本的な構造 構造に則った開発方法 GraphQL Figma 例 OpenAPI CODE GENERATOR Protocol Buffer 入力元となる情報 なにから ルール(Schema)に従って構築されている。 Code Generator 性能が出力するコード 品質を決める 出力
ほしい機能を搭載した コードジェネレーターがない?
Code Generator 実装方法
2.2 コードジェネレーター実装入門 今回紹介するCode Generator Parse Source Codeを ASTに変換 作り方 Traverse 木 走査をしながら Nodeを変換 unparse ASTから Source Codeに変換
2.2 コードジェネレーター実装入門 “Hello World”という文字列を解析する 1/2 Source Text コードを入力 Tree Viewer Source Text 抽象木 Factory Code 生成コード Property Viewer 選択した抽象木 Node Property
2.2 コードジェネレーター実装入門 “HELLO WORLD!” factoryコード factory.createExpressionStatement( factory.createStringLiteral("HELLO WORLD!") );
2.2 コードジェネレーター実装入門 ASTからコードを生成するため 実装 import * as fs from "fs"; import ts from "typescript"; const sourceFile = ts.createSourceFile("", "", ts.ScriptTarget.ESNext); Source File 作成 (空ファイル) const transformedSourceFile = ts.factory.updateSourceFile( sourceFile, [ ts.factory.createExpressionStatement( ts.factory.createStringLiteral("HELLO WORLD!") ), Source Fileにstatements ASTを追加していく ], sourceFile.isDeclarationFile, sourceFile.referencedFiles, sourceFile.typeReferenceDirectives, sourceFile.hasNoDefaultLib, sourceFile.libReferenceDirectives ); const printer = ts.createPrinter(); const code = printer.printFile(transformedSourceFile); fs.writeFileSync("output/sample1.ts", code, "utf-8"); ASTからstringに変換
、簡単でしょう?
…
本当か?
2.2 コードジェネレーター実装入門 Allow function const hello = () => { return "world"; } 例
2.2 コードジェネレーター実装入門
Allow function
例
多い...!
[
const hello = () => {
factory.createVariableStatement(
return "world";
undefined,
factory.createVariableDeclarationList(
}
[factory.createVariableDeclaration(
factory.createIdentifier("hello"),
undefined,
undefined,
右 ようにAST
Factoryコードが書ける
factory.createArrowFunction(
undefined,
undefined,
[],
undefined,
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
factory.createBlock(
[factory.createReturnStatement(factory.createStringLiteral("world"))],
true
)
)
)],
ts.NodeFlags.Const | ts.NodeFlags.Constant | ts.NodeFlags.Constant
)
)
];
経験則から得られた アンチパターンとそ 対策
2.3 コードジェネレーター実装入門 何を経験した ? ● ● ● ● Himenon/openapi-typescript-code-generatorを4年運用 ○ 作った動機 前述した通り、当時開発中 プロジェクトに適したも がな かったから作った OpenAPI to TypeScript Code GeneratorをASTで全部やるよというライブラリ ○ 前述したAllow Function 比じゃない量が存在する 不正な入力値も可能な限り推論を内部的に行い、コード生成する 4年間でちまちまとバグ報告 Issueが立つたびに修正を繰り返してきた
2.3 コードジェネレーター実装入門 Q. ASTでFactoryコードを書く が大変な で ?
2.3 コードジェネレーター実装入門 Q. ASTでFactoryコードを書く が大変な A. で ? YES 普段利用しないパラメーターも記述す る必要があるため、冗長なコードをたく さん書くことになる 経験則 確実にWrapperを書きたくなる 余談:なんでフラットな引数な ?とDanielに直接聞いたら、パフォーマ ンス 観点から話をしてくれました。メモリアロケーション(GCなど)でフ ラットな引数が有効。ASTを直接使う人に not friendryだよ 〜と言う 話が聞けました。
2.3 コードジェネレーター実装入門
行き着く先
1.
2.
3.
... ASTとPlain Textをハイブリットに書きたい - 3つ
方法
Factory Code → AST → Plain Text(出力)
Plain Text → parse → AST → Plain Text(出力)
ASTに変換せず、成果物 stringで結合
方法2: SourceFile
statementsを抽出
方法2: ts-morphを使う
import ts from "typescript";
import { Project, ts } from "ts-morph";
const stringToStatements = (code: string): ts.Statement[] => {
const text = `const hello = () => {
const source = ts.createSourceFile("", code, ts.ScriptTarget.ESNext,
false, ts.ScriptKind.TS);
return "world";
}`;
return Array.from(source.statements);
};
const project = new Project();
const sourceFile = project.createSourceFile("sample.ts", "");
const text = `const hello = () => {
return "world";
sourceFile.addStatements([text]);
}`;
project.saveSync();
stringToStatements(text);
2.3 コードジェネレーター実装入門 コード テンプレート部分にASTを使うかStringを使うか 比較項目 AST String Literal 実装量 String Literalより多い Template Literalを使うと最小にできる 独自構文にな るか? AST い 組み立てルールに従う でならな String 組み合わせになってくるため、ポータ ビリティ 確保 開発者が定義する 経験則 ● ● ● Code Generateした量がディスプレイ1枚に収まる or パターンがない場合 ASTを使う利点 薄い ASTでCode Generateを書く利点 実装が独自にならないこと。4年前 コードでも思い出せる。 ASTでコードを生成した場合、括弧 扱い{ } ( ) が無いため。出力するコード 階層化するときに余計なこと を考えなくて良い。
コードジェネレーター実装入門 まとめ ● ASTからコードを生成するする方法を紹介した ● AST ● ASTだけでコード生成をすると実装量が増えてしまう で、 Stringとハイブリットに使う ● ASTを使ったCode Generator 出せる Factoryコードを書いてしまえ unparseするだけでコードが生成できる 原理的な実装に基づくため数ヶ月立って見直しても思い
と、本当 (論理的に)まとめたいが
2.1 コード生成入門 Code Generator Code Generator ● ● 前後で2つ 解決する課題とそ 基本的な構造 事象が顕になる 出力側 ○ 成果物 TypeScriptで固定される ○ 生成されるコード 品質が均一になる 入力側 ○ 大抵 ケースで仕様が決まった入力をCode Generatorに渡せ よい(Code Generator 開発者が少ない 都合を気にしないでよい) 無数 選択肢が生まれる 依存方向 ? CODE GENERATOR 入力 仕様やSchema 出力 コード生成機 ※AST 利用 有無 ここで ? 成果物 関係ない ソースコードなど
2.1 コード生成入門 Code Generator Code Generator ● ● 前後で2つ 解決する課題とそ 基本的な構造 事象が顕になる 出力側 ○ 成果物 TypeScriptで固定される ○ 生成されるコード 品質が均一になる 入力側 ○ 大抵 ケースで仕様が決まった入力をCode Generatorに渡せ よい(Code Generator 都合を気にしないでよい) 依存方向(こういう方向もいつか) ? CODE GENERATOR 入力 仕様やSchema 出力 コード生成機 ※AST 利用 有無 ここで ? 成果物 関係ない ソースコードなど
「easy 便利だが、simple を使うことで原理を理解できる。 原理を理解したら、便利(easy)なツールを求めるようになる」 JSConfJP 2019 t-wadaさ セッションより
コードジェネレーター実装入門 まとめ2 ● コードジェネレーターを作っている開発者、そう多くない → コードジェネレーター バラエティが少ない ● ASTを使うと良くわかっていない文法によく遭遇する → 創造できる世界が広がる (コードコメントってAST上でどうやって表現されいるか?)
目次 1. TypeScript AST入門 12分 2. コードジェネレーター実装入門 15分 3. コードジェネレーター 実装ルーチン 3分
コードジェネレーター 実装ルーチン
Code Generator
開発サイクル
1.
入力 仕様を決める(すでにある仕様を使うでも OK)
2.
Factory化するコードを決めてどんどん書いていく
○
ts-ast-viewerを使うとFactory
コードをコピペできて効率的
3.
Code Generator
テスト
4.
生成されたコード
Type Checkとsnapsthotテストで十分
、
import * as fs from "fs";
test("Code Generate Test", () => {
const generateCode = fs.readFileSync("generated.ts", { encoding: "utf-8" });
expect(generateCode).toMatchSnapshot();
});
入力データ 実際に利用するも を用意。 種類が多けれ 多いほどよい https://github.com/Himenon/openapi-typescript-code-generator/tree/main/test
生成コードをsnapshotする 生成されたコード 別途 tsc –noEmitを 実施して型チェックしておく
時間があれ デモデモ https://github.com/Himenon/tskaigi-2024-code-sample
全体 まとめ
全体まとめ TypeScript ● ● Abstract Syntax Tree入門 ソースコードをプログラマブルにした木 データ構造 木構造に対する実装パターンを利用することができる Code Generator入門 ● ● ● ● ● AST Factoryコードをどんどん書く。 入力 部分をパラメーター化すれ Code Generatorとして機能する ASTだけ使うと実装量が増えるため、 Stringハイブリッドで実装とよい テスト スナップショットテストで十分 とりあえずASTをつかってCode Generator書いてみて。 実装ルーチン ● ● AST Factoryコード AST Viewerを使うと効率的 テスト 生成コードに対するスナップショットテストと型チェックで十分