2022年6月1日水曜日

Langium で DSL と LSP サーバーを作る(2) AST をもとにコード生成を行う

前回 の続き。 AST からコード生成を行う。

目標

以下の定義を元に、

person mikoto {
    age: 18;
    rank: 1;
}

person makoto {
    age: 19;
    rank: 2;
}

person mokoto {
    age: 20;
    rank: 3;
}

以下のコードを生成する。

export type Person = {
    age : number;
    rank : number;
}

const mikoto : Person = {
    age: 18,
    rank: 1
}

const makoto : Person = {
    age: 19,
    rank: 2
}

const mokoto : Person = {
    age: 20,
    rank: 3
}

export const allPersons = [mikoto, makoto, mokoto];

前提

  • OS: Arch Linux
  • Docker: Docker version 20.10.16, build aa7e414fdc
  • 使用する Docker イメージ: node:18

作業用コンテナ起動

docker run -it --rm -v "$(pwd):/work" --workdir /work node:18 bash

DLS 開発(コード生成まで)

コード生成機能実装

src/cli/generator.ts を作成し、「person 定義ごとに定数を作り、全 person が含まれた配列を作成」というコードを生成する実装を行う。

src/cli/generator.ts

import fs from 'fs';
import { CompositeGeneratorNode, NL, processGeneratorNode } from 'langium';
import path from 'path';
import { Model } from '../language-server/generated/ast';
import { extractDestinationAndName } from './cli-util';

// Person 型の定義
const TYPE_PERSON = `export type Person = {
    age : number;
    rank : number;
}`;

export function generateTypeScript(model: Model, filePath: string, destination: string | undefined): string {
    const data = extractDestinationAndName(filePath, destination);
    const generatedFilePath = `${path.join(data.destination, data.name)}.ts`;

    // これから生成するファイルの内容を表すインスタンスを生成
    const fileNode = new CompositeGeneratorNode();

    // 型定義コードを追加
    // TYPE_REASON 定数と、改行をふたつ
    fileNode.append(TYPE_PERSON, NL, NL);

    // DSL の person 定義ごとに Person 型の定数を追加
    model.persons.forEach(person => {
        fileNode.append(`const ${person.name} : Person = {
    age: ${person.age},
    rank: ${person.age}
}`, NL, NL)
    });

    // 全 person が含まれた Array を追加
    const allPersons = model.persons.map(person => person.name).join(', ');
    fileNode.append(`export const allPersons = [${allPersons}];`);


    // 生成先ディレクトリがなければ作る
    if (!fs.existsSync(data.destination)) {
        fs.mkdirSync(data.destination, { recursive: true });
    }

    // 生成先にファイル書き出し
    fs.writeFileSync(generatedFilePath, processGeneratorNode(fileNode));

    // 生成したファイルのファイルパスを返却
    return generatedFilePath;
}

cli からコード生成機能を呼び出す実装を追加

src/cli/index.ts

diff --git a/src/cli/index.ts b/src/cli/index.ts
index e0a4230..40d2e4c 100644
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -4,6 +4,14 @@ import { Model } from '../language-server/generated/ast';
 import { FirststepLanguageMetaData } from '../language-server/generated/module';
 import { createFirststepServices } from '../language-server/firststep-module';
 import { extractAstNode } from './cli-util';
+import { generateTypeScript } from './generator';
+
+export const generateAction = async (fileName: string, opts: GenerateOptions): Promise<void> => {
+    const services = createFirststepServices().Firststep;
+    const model = await extractAstNode<Model>(fileName, services);
+    const generatedFilePath = generateTypeScript(model, fileName, opts.destination);
+    console.log(colors.green(`TypeScript code generated successfully: ${generatedFilePath}`));
+};
 
 export const testAction = async (fileName: string, opts: GenerateOptions): Promise<void> => {
     const services = createFirststepServices().Firststep;
@@ -24,6 +32,13 @@ export default function(): void {
         .version(require('../../package.json').version);
 
     const fileExtensions = FirststepLanguageMetaData.fileExtensions.join(', ');
+    program
+        .command('generate')
+        .argument('<file>', `source file (possible file extensions: ${fileExtensions})`)
+        .option('-d, --destination <dir>', 'destination directory of generating')
+        .description('generates TypeScript code that all Person array.')
+        .action(generateAction);
+
     program
         .command('test')
         .argument('<file>', `source file (possible file extensions: ${fileExtensions})`)
@@ -32,3 +47,4 @@ export default function(): void {
 
     program.parse(process.argv);
 }

相対パスでファイルを指定するとデータ名が空になる問題の修正

src/cli/cli-util.ts を修正。

diff --git a//src/cli/cli-util.ts b//src/cli/cli-uti
l.ts
index 53f6a94..b67cab6 100644
--- a//src/cli/cli-util.ts
+++ b//src/cli/cli-util.ts
@@ -43,7 +43,7 @@ interface FilePathData {
 }
 
 export function extractDestinationAndName(filePath: string, destination: string | undefined): FilePathDat
a {
-    filePath = filePath.replace(/\..*$/, '').replace(/[.-]/g, '');
+    filePath = path.basename(filePath, path.extname(filePath)).replace(/[.-]/g, '');
     return {
         destination: destination ?? path.join(path.dirname(filePath), 'generated'),
         name: path.basename(filePath)

ビルド

npm run build

コード生成動作確認

「目標」で示した DSL 定義ファイルを作成し、コード生成を行う。

cat << EOF >> test.firststep
person mikoto {
    age: 18;
    rank: 1;
}

person makoto {
    age: 19;
    rank: 2;
}

person mokoto {
    age: 20;
    rank: 3;
}
EOF

node bin/cli generate -d ./generated_code ./test.firststep

これで、 generated_code/test.ts が生成される。

# cat generated_code/test.ts
export type Person = {
    age : number;
    rank : number;
}

const mikoto : Person = {
    age: 18,
    rank: 18
}

const makoto : Person = {
    age: 19,
    rank: 19
}

const mokoto : Person = {
    age: 20,
    rank: 20
}

export const allPersons = [mikoto, makoto, mokoto];

今回はここまで。

参考資料

0 件のコメント:

コメントを投稿