TSKaigi 2024 TypeScript ASTを利用したコードジェネレーターの実装入門

32K 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年間維持し続けている経験から紹介していきます。

profile-image

HireRooは、エンジニア採用のコーディング試験サービスです。🦘エンジニアの技術力を多角的かつ定量的に評価することで、候補者と企業のミスマッチを防ぎます。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

TypeScript ASTを利用した コードジェネレーター 実装入門 Track 2 11:30 ~ 12:00 @Himenon

2.

自己紹介 SNS ● ● GitHub: Himenon Twitter: himenoglyph 所属 ● 株式会社ハイヤールー(Coding Interview platform) エンジニアリング領域 ● ● Webフロントエンド(メイン) 開発支援ツールを作る が趣味 代表的なOSS ● Himenon/openapi-typescript-code-generator ← こ 知見が本発表 ベース!

3.

本発表で利用するサンプルコード github.com/Himenon/tskaigi-2024-code-sample

4.

目次 1. TypeScript AST入門 12分 2. コードジェネレーター実装入門 15分 3. コードジェネレーター 実装ルーチン 3分

5.

目次 1. TypeScript AST入門 12分 2. コードジェネレーター実装入門 15分 3. コードジェネレーター 実装ルーチン 3分

6.

1. TypeScript Abstract Syntax Tree入門

7.

TypeScript AST入門 TypeScript Abstract Syntax Tree(抽象構文木)と ? 一言で説明すると、 ソースコードをプログラマブルにしたデータ構造

8.

TypeScript AST入門 AST(Abstract Syntax Tree)と他 TypeScript APIと 関係性 Compiler API 変換・ Type Check Language Server protocol コード補完・構 文チェック 変換後 コード 利用 Source Code (Plain Text) 解析 Parse AST 利用 TypeScript 操作をしようとすると最初に出てくる基本知識 IDE/エディタ

9.

TypeScript AST入門 TypeScript AST Viewerを使ってASTを見てみる ブラウザでASTを確認するため https://ts-ast-viewer.com ツール

10.

TypeScript AST入門 “Hello World”という文字列を解析する

11.

TypeScript AST入門 “Hello World”という文字列を解析する 1/2 ①Source Codeを入力 ② Source CodeをPrase結果 Tree Viewer Source Text 抽象構文木 ④AST Factory Code 生成コード ③Property Viewer 選択した抽象木 Node Property

12.

TypeScript AST入門 “Hello World” AST “Hello World”; パースされた AST SourceFile ● 文法 / 種別 ファイル ExpressionStatement ○ StringLiteral 式文 文字列 EndofFileToken ファイル 終了識別子

13.

TypeScript AST入門 “Hello” + “World” AST “Hello”; SourceFile “World”; ● ● ExpressionStatement ○ StringLiteral ExpressionStatement ○ StringLiteral EndofFileToken “Hello”に対して “World”に対して

14.

TypeScript AST入門 少し複雑な実装 const mainTask = () => { const subTask = () => {} return subTask(); AST SourceFile ● } mainTask(); ● VariableSatement ○ VariableDeclaration ○ ArrowFunction ■ Block ● VariableStatement ■ ReturnStatement ● CallExpression ExpressionStatement ○ CallExpression

15.

TypeScript AST入門 ASTを可視化すると const mainTask = () => { SourceFile const subTask = () => {} return subTask(); } VariableStatement const mainTask = mainTask(); VariableDeclaration const subTask = ArrowFunction () => { … } EqualsGreaterThanToken Block ExpressionStatement mainTask(); ArrowFucntion

16.

TypeScript AST入門 Abstract Syntax Tree(抽象構文木)と周辺知識 ● ソースコードが文法や抽象化される ○ ● 式、文、ブロックなど普段見えない文法構造がデータとして扱える データ構造として 木構造を取っている root 木構造を扱うアルゴリズムが ASTを取り扱う文脈で出てくる用語 ● ● ● ● root node 幅優先探索(BFS) / 深さ優先探索( DFS) Visitorパターン node

17.

TypeScript AST入門 木構造 取り扱い - 走査(traverse) 走査 各nodeを順番に訪問(visit)すること 1 訪問したnodeに対して変換処理をすること ができる 2 3 4 6 5 7 8

18.
[beta]
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していく

19.

TypeScript AST入門 ASTを扱う工程 3ステップ Parse Source Codeを ASTに変換 Traverse 木 走査をしながら Nodeを変換 unparse ASTから Source Codeに変換

20.

目次 1. TypeScript AST入門 12分 2. コードジェネレーター実装入門 15分 3. コードジェネレーター 実装ルーチン 3分

21.

2. コードジェネレーター実装入門

22.

コードジェネレーター実装入門 こ 章 次 内容を含みます 1. Code Generator 解決する課題とそ 基本的な構造 2. Code Generator 実装方法 3. 経験則から得られたアンチパターンとそ 対策

23.

2.1 コードジェネレーター実装入門 Code Generator Code Generator ● ● 前後で2つ 解決する課題とそ 基本的な構造 事象が顕になる 出力側 ○ 成果物 TypeScriptで固定される ○ 生成されるコード 品質が均一になる 入力側 ○ 大抵 ケースで仕様が決まった入力をCode Generatorに渡せ ? よい(Code Generator CODE GENERATOR 入力 仕様やSchema 出力 コード生成機 ※AST 利用 有無 ここで 都合を気にしないでよい) ? 成果物 関係ない ソースコードなど

24.

2.1 コードジェネレーター実装入門 Code Generator Code Generator ● ● 前後で2つ 解決する課題とそ 基本的な構造 事象が顕になる 出力側 ○ 成果物 TypeScriptで固定される ○ 生成されるコード 品質が均一になる 入力側 ○ 大抵 ケースで仕様が決まった入力をCode Generatorに渡せ よい(Code Generator 都合を気にしないでよい) 依存方向 ? CODE GENERATOR 入力 仕様やSchema 出力 コード生成機 ※AST 利用 有無 ここで ? 成果物 関係ない ソースコードなど

25.

2.1 コードジェネレーター実装入門 Code Generator Code Generator ● ● 前後で2つ 解決する課題とそ 基本的な構造 事象が顕になる 出力側 ○ 成果物 TypeScriptで固定される ○ 生成されるコード 品質が均一になる 入力側 ○ 大抵 ケースで仕様が決まった入力をCode Generatorに渡せ よい(Code Generator 開発者が少ない 都合を気にしないでよい) 無数 選択肢が生まれる 依存方向 ? CODE GENERATOR 入力 仕様やSchema 出力 コード生成機 ※AST 利用 有無 ここで ? 成果物 関係ない ソースコードなど

26.

2.1 コードジェネレーター実装入門 Code Generator Code Generator 解決する課題とそ 基本的な構造 構造に則った開発方法 GraphQL Figma 例 OpenAPI CODE GENERATOR Protocol Buffer 入力元となる情報 なにから ルール(Schema)に従って構築されている。 Code Generator 性能が出力するコード 品質を決める 出力

27.

ほしい機能を搭載した コードジェネレーターがない?

28.

Code Generator 実装方法

29.

2.2 コードジェネレーター実装入門 今回紹介するCode Generator Parse Source Codeを ASTに変換 作り方 Traverse 木 走査をしながら Nodeを変換 unparse ASTから Source Codeに変換

30.

2.2 コードジェネレーター実装入門 “Hello World”という文字列を解析する 1/2 Source Text コードを入力 Tree Viewer Source Text 抽象木 Factory Code 生成コード Property Viewer 選択した抽象木 Node Property

31.

2.2 コードジェネレーター実装入門 “HELLO WORLD!” factoryコード factory.createExpressionStatement( factory.createStringLiteral("HELLO WORLD!") );

32.

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に変換

33.

、簡単でしょう?

34.

35.

本当か?

36.

2.2 コードジェネレーター実装入門 Allow function const hello = () => { return "world"; } 例

37.
[beta]
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
)
)
];

38.

経験則から得られた アンチパターンとそ 対策

39.

2.3 コードジェネレーター実装入門 何を経験した ? ● ● ● ● Himenon/openapi-typescript-code-generatorを4年運用 ○ 作った動機 前述した通り、当時開発中 プロジェクトに適したも がな かったから作った OpenAPI to TypeScript Code GeneratorをASTで全部やるよというライブラリ ○ 前述したAllow Function 比じゃない量が存在する 不正な入力値も可能な限り推論を内部的に行い、コード生成する 4年間でちまちまとバグ報告 Issueが立つたびに修正を繰り返してきた

40.

2.3 コードジェネレーター実装入門 Q. ASTでFactoryコードを書く が大変な で ?

41.

2.3 コードジェネレーター実装入門 Q. ASTでFactoryコードを書く が大変な A. で ? YES 普段利用しないパラメーターも記述す る必要があるため、冗長なコードをたく さん書くことになる 経験則 確実にWrapperを書きたくなる 余談:なんでフラットな引数な ?とDanielに直接聞いたら、パフォーマ ンス 観点から話をしてくれました。メモリアロケーション(GCなど)でフ ラットな引数が有効。ASTを直接使う人に not friendryだよ 〜と言う 話が聞けました。

42.
[beta]
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);

43.

2.3 コードジェネレーター実装入門 コード テンプレート部分にASTを使うかStringを使うか 比較項目 AST String Literal 実装量 String Literalより多い Template Literalを使うと最小にできる 独自構文にな るか? AST い 組み立てルールに従う でならな String 組み合わせになってくるため、ポータ ビリティ 確保 開発者が定義する 経験則 ● ● ● Code Generateした量がディスプレイ1枚に収まる or パターンがない場合 ASTを使う利点 薄い ASTでCode Generateを書く利点 実装が独自にならないこと。4年前 コードでも思い出せる。 ASTでコードを生成した場合、括弧 扱い{ } ( ) が無いため。出力するコード 階層化するときに余計なこと を考えなくて良い。

44.

コードジェネレーター実装入門 まとめ ● ASTからコードを生成するする方法を紹介した ● AST ● ASTだけでコード生成をすると実装量が増えてしまう で、 Stringとハイブリットに使う ● ASTを使ったCode Generator 出せる Factoryコードを書いてしまえ unparseするだけでコードが生成できる 原理的な実装に基づくため数ヶ月立って見直しても思い

45.

と、本当 (論理的に)まとめたいが

46.

2.1 コード生成入門 Code Generator Code Generator ● ● 前後で2つ 解決する課題とそ 基本的な構造 事象が顕になる 出力側 ○ 成果物 TypeScriptで固定される ○ 生成されるコード 品質が均一になる 入力側 ○ 大抵 ケースで仕様が決まった入力をCode Generatorに渡せ よい(Code Generator 開発者が少ない 都合を気にしないでよい) 無数 選択肢が生まれる 依存方向 ? CODE GENERATOR 入力 仕様やSchema 出力 コード生成機 ※AST 利用 有無 ここで ? 成果物 関係ない ソースコードなど

47.

2.1 コード生成入門 Code Generator Code Generator ● ● 前後で2つ 解決する課題とそ 基本的な構造 事象が顕になる 出力側 ○ 成果物 TypeScriptで固定される ○ 生成されるコード 品質が均一になる 入力側 ○ 大抵 ケースで仕様が決まった入力をCode Generatorに渡せ よい(Code Generator 都合を気にしないでよい) 依存方向(こういう方向もいつか) ? CODE GENERATOR 入力 仕様やSchema 出力 コード生成機 ※AST 利用 有無 ここで ? 成果物 関係ない ソースコードなど

48.

「easy 便利だが、simple を使うことで原理を理解できる。 原理を理解したら、便利(easy)なツールを求めるようになる」 JSConfJP 2019 t-wadaさ セッションより

49.

コードジェネレーター実装入門 まとめ2 ● コードジェネレーターを作っている開発者、そう多くない → コードジェネレーター バラエティが少ない ● ASTを使うと良くわかっていない文法によく遭遇する → 創造できる世界が広がる (コードコメントってAST上でどうやって表現されいるか?)

50.

目次 1. TypeScript AST入門 12分 2. コードジェネレーター実装入門 15分 3. コードジェネレーター 実装ルーチン 3分

51.

コードジェネレーター 実装ルーチン

52.
[beta]
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();
});

53.

入力データ 実際に利用するも を用意。 種類が多けれ 多いほどよい https://github.com/Himenon/openapi-typescript-code-generator/tree/main/test

54.

生成コードをsnapshotする 生成されたコード 別途 tsc –noEmitを 実施して型チェックしておく

55.

時間があれ デモデモ https://github.com/Himenon/tskaigi-2024-code-sample

56.

全体 まとめ

57.

全体まとめ TypeScript ● ● Abstract Syntax Tree入門 ソースコードをプログラマブルにした木 データ構造 木構造に対する実装パターンを利用することができる Code Generator入門 ● ● ● ● ● AST Factoryコードをどんどん書く。 入力 部分をパラメーター化すれ Code Generatorとして機能する ASTだけ使うと実装量が増えるため、 Stringハイブリッドで実装とよい テスト スナップショットテストで十分 とりあえずASTをつかってCode Generator書いてみて。 実装ルーチン ● ● AST Factoryコード AST Viewerを使うと効率的 テスト 生成コードに対するスナップショットテストと型チェックで十分