👀

Rust の SWC で TypeScript のコードの解析と生成をする

2023/03/08に公開

Rust の SWC というライブラリで TypeScript のコードの解析と生成をします.

https://swc.rs/

解析

/** アカウント */
export type Account = {
    /** アカウントを識別するID */
    readonly id: string;
    /** アカウント名 */
    readonly name: string;
}

この TypeScript のコードに含まれる型のデータを解析し, 次のようなデータを得ることを目標とします.

[
    TypeData {
        name: "Account",
        comment: "アカウント",
        members: [
            MemberData {
                name: "id",
                comment: "アカウントを識別するID",
            },
            MemberData {
                name: "name",
                comment: "アカウント名",
            },
        ],
    },
]

次のコードで取得することができました.

https://github.com/narumincho/rust-swc-test/blob/6f0377ec3a098d5bfdcfe1b16ebfef465611cf42/src/main.rs#L5-L118

出力

[
    TypeData {
        name: "Account",
        comment: "* アカウント ",
        members: [
            MemberData {
                name: "id",
                comment: "* アカウントを識別するID ",
            },
            MemberData {
                name: "name",
                comment: "* アカウント名 ",
            },
        ],
    },
]

コメントに * と末尾の空白が入ってる問題はありますが、とりあえずできました. SWC 的には ドキュメンテーションコメントかどうかは区別しないようです.

おそらく特定の部分木を探すみたいな方法がありそうですが, シンプルにパターンマッチングをして取り出しました. そのため, このコードではモジュール内に含まれている型の定義は取得できません.

コメントの部分が, 木構造に含まれていないため別で

https://github.com/narumincho/rust-swc-test/blob/6f0377ec3a098d5bfdcfe1b16ebfef465611cf42/src/main.rs#L51-L57

このように取得することになります.

また, swc_ecma_ast::TsType::TsTypeLitswc_ecma_ast::TsType::TsLitType の名前が似ているので注意が必要です.

swc_ecma_ast::TsType:: TypeScript のコード
TsTypeLit { id: string, readonly name: string }
TsLitType "sample"28

生成

今度は逆に

&TypeData {
    name: "Account".to_string(),
    comment: "アカウント".to_string(),
    members: vec![
        MemberData {
            name: "id".to_string(),
            comment: "* アカウントを識別するID ".to_string(),
        },
        MemberData {
            name: "name".to_string(),
            comment: "* アカウント名 ".to_string(),
        },
    ],
}

を入力として, 以下の TypeScript のコードを生成するコードを作成します

/** アカウント */
export type Account = {
  /** アカウントを識別するID */
  readonly id: string;
  /** アカウント名 */
  readonly name: string;
};

以下のコードで出力することができました.

https://github.com/narumincho/rust-swc-test/blob/6f0377ec3a098d5bfdcfe1b16ebfef465611cf42/src/main.rs#L107-L231

出力

/*アカウント*/ export type Account = {
    /** アカウントを識別するID */ readonly id: string;
    /** アカウント名 */ readonly name: string;
};

コードフォーマットは してくれないようです.

コメントはやはりswcの構文木に含まれていないので, 別で追加しています.

swc_common::BytePos が位置の指定, swc_common::Spanned::span(&BytePos) によってコード範囲の swc_common::Span に変換できます. BytePos という名前ですが, 出力先のコードのバイト位置にする必要はないみたいです.

  • swc_common::BytePos(0)
  • swc_common::BytePos::DUMMY

の指定をしたコメントは出力時に無視されました. 0番目は予約されているみたいです. Option では だめな理由があるのだろうか...

BytePos のカウント方法

SWC のコード ( https://rustdoc.swc.rs/src/swc_common/syntax_pos.rs.html#581 ) を見ると, GLOBALS というグローバルの構造体?の値を1ずつ増やしてインデックスのカウントを増やしているようです.

自前で足していくよりも 良い方法があるような気がしますが, うまくいきませんでした.

swc_common::Span::dummy_with_cmt(); 

実行した結果のエラー↓

thread 'type_data_to_code_main' panicked at 'You should perform this operation in the closure passed to `set` of swc_common::syntax_pos::GLOBALS'

また, SWC のドキュメントは https://docs.rs/ ではなく https://rustdoc.swc.rs/swc_common/source_map/struct.Span.html#method.dummy_with_cmt で公開している. ビルドの都合上そうなってしまうのか?

追記

swc_common::GLOBALS.set(&swc_common::Globals::default(), || {
  // swc_common::Span::dummy_with_cmt() が使える
}

のようにすることによって, swc_common::Span::dummy_with_cmt() が使えるようになることが判明しました.

https://rust-lang-jp.zulipchat.com/#narrow/stream/124300-questions/topic/SWC.20.E3.81.A7.E3.82.B3.E3.83.A1.E3.83.B3.E3.83.88.E3.81.AE.E7.94.9F.E6.88.90.E3.81.A7.E3.82.A4.E3.83.B3.E3.83.87.E3.83.83.E3.82.AF.E3.82.B9.E3.81.AE.E7.AE.A1.E7.90.86.E3.82.92.E3.81.97.E3.81.AA.E3.81.8F.E3.81.A6.E3.81.99.E3.82.80.E6.96.B9.E6.B3.95

よってコードは次のように変更しました.

https://github.com/narumincho/rust-swc-test/blob/a2ae63f42e6a00e23d9b4caf8c4bbbaa9163e6a1/src/main.rs#L107-L231

この仕様はグローバル変数的な使い方をしたい場合に使う?ライブラリの scoped_tls によるものでした. この scoped_tls を エラーメッセージが若干分かりやすいように SWC が改良したのが better_scoped_tls みたいです

https://docs.rs/scoped-tls/1.0.1/scoped_tls/index.html

その他, より良い方法を見つけたらコメントをしてくれると嬉しいです.

GitHubで編集を提案

Discussion