error-reporter.png
Apr 4, 2020

TypeScriptの型エラーをPR上に表示するGitHub Actionsを作成してみた

GitHub Actionsについて調べていたら、ひょんなことからworkflow commandsの存在とそれを使ってPull RequestのDiff Viewでメッセージが表示できることを知った。 (GitHub ActionsでESLintを動かした時にエラー表示してくれるアレ)

良い機会なのでTS Compiler APIの勉強も兼ねて型検査の結果をコード上に表示するためのGitHub Actionsを自作しようと思う。

できたもの

Embedded content: https://github.com/marketplace/actions/typescript-error-reporter

Marketplace

使い方

お手持ちのGitHub Actions workflowに2行追加するだけ

- name: TypeScript Error Reporter
  uses: andoshin11/typescript-error-reporter-action@v1.0.0

キャッシュも含めてフルで書くとこんな感じ

name: main

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12.13.0]
    steps:
      - uses: actions/checkout@v1
      - name: Prepare repository
        run: git checkout "${GITHUB_REF:11}"
      - name: Setting up Node.js v${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - name: Restore dependencies
        uses: actions/cache@v1
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-node-
      - name: Install dependencies
        run: |
          yarn install
      - name: TypeScript Error Reporter
        uses: andoshin11/typescript-error-reporter-action@v1.0.0
        env:
          CI: true

上記のように設定すると、Pull Request作成のタイミングで自動でCI上で型チェックが走るはず。

job failing

型エラーがあればJobがfailする

error-reporter

エラー部分はDiff viewでエラー内容がコメントされる。

手元のTypeScriptプロジェクト3つほどで試しに動かしてみましたが問題なさそうでした。 不具合があれば下記のRepositoryまでIssueください。

Embedded content: https://github.com/andoshin11/typescript-error-reporter-action

How it works?

本ツールがやっていることは主に3つ

  • Step 0: コンパイラの初期化
  • Step 1: コンパイル結果( diagnostics)の取得
  • Step 2: Step 1の結果の整形およびGitHubへのレポート

Step 0: コンパイラの初期化

初期化処理としてTypeScriptの Language Serviceおよびその内部で利用する Language Service Hostを作成する。

TSの Language Serviceが受け取ることのできるHostは非常に柔軟性が高く、TS Programが行うファイルアクセス処理(ディレクトリ取得、ファイル読み取り等)に対してかなりアグレッシブな介入を行うことができるのが特徴だ。

世のTS Compiler APIを利用したツールでよく見られる介入処理としては、メモリ上の仮想ファイルを媒介にしたRead高速化や本来TSが解釈できないファイルフォーマット( .vue等)へのresolverの提供、 appendSuffix処理等があり、エンジニアとしてはかなり創造性が刺激される仕組みの一つである。

Step 1: コンパイル結果( diagnostics)の取得

先のStepで作成した Language ServiceからTS Programの実態を取り出し静的解析を実行する。

このTS Programからは本ツールで利用している Semantic Diagnosticsの他に Global Diagnostics, Syntactic Diagnosticsといった多様な解析結果を取得できるだけでなく、 ASTの取得やAST Nodeから型情報を取り出せる Type Checkerの取得といった様々な便利な情報を利用できる。もちろんEmitコマンドを実行すればコンパイル後の d.tsファイルや .jsファイルの取得も可能だ。

また、TS Programの解析結果( diagnostics)に含まれる内容はエラーや警告文の他に Suggestionというカテゴリも存在する。 今回の本旨からは逸れるが、この Suggestion DiagnosticsからASTに対するCodeFix処理を自動適用可能なツールも着手中。柔軟にユーザー入力を受け取ったりDiagnosticsからの変換処理を行ってCodeFixを簡単にできないかなーと夢想している。

Step 2: 実行結果の整形およびGitHubへのレポート

GitHub Actions内からはworkflow commandsという仕組みを利用することができ、これらを用いて後続Actionへの値の受け渡しやUI上へのレポーティングといったリッチな処理を行うことができる。

Commandはリテラルの命令文で発行することも可能だが、公式の @actions/coreが提供する issueCommandを利用するのが個人的にオススメ。

TS Programの提供する diagnosticsにはコード上の位置情報も含まれるため、今回はそれをGitHub向けのcode location formatに変換することで対応した。

こだわりポイント

異なるTSバージョンへの対応

本ツールの実装には記事執筆時点で最新である typescript@3.8.3を利用しているが、CI向けのツールとして提供する以上は各プロジェクトで利用されているバージョンのTypeScriptに解析処理は委ねたいところである。

ならば実行コンテキストで node_modules/typescript/lib/typescript.jsを探索して requireすれば良いかというと、それはそれでユーザーに事前の $ npm installを強制してしまうため少々悩ましい。汎用的なGitHub Actionsとして公開する性質上、外部のコンテキストに依存することなく単独での実行を可能にするのがベストだ。

そのような理由を踏まえて今回はlockファイルから解析したバージョンの typescript.jsをCDN上から取得することにした。ネットワーク経由で取得したJSファイルをevalするというのは若干の気持ち悪さがあるが、一応の妥協点とする。

fetch from CDN

lib.d.tsの読み込み

上記のようにTypeScriptの本体は実行環境に合わせてCDN上から取得することとした。これによって一般的なJavaScriptランタイム上でCompiler APIを実行できるようになったわけである。めでたしめでたし。






とまぁそうは問屋が卸さないわけで、次に問題となるのが node_modules/typescript/lib配下にある lib.*.d.tsといったファイルたちだ。

一般的なECMAScriptの型定義(Promise, Array#map, Object#entries, etc..)はこれらのファイル内に定義されているためコンパイラの実行時にそれらが存在しなければ、 "Cannot find global type 'Promise'"というようなエラーが膨大に表示されてしまうのである。

コンパイラの実行ファイル自体はCDNで取得できるが、適切な実行結果を得るにはnpmを経由してこれらの lib.*.d.ts filesを取得しなければならないという奇妙な状況は、まさにTypeScriptならではの問題だ。

解決策

そこで今回は苦肉の策として lib.*.d.ts自体は手元の typescript@3.8.3環境のものをGitHub Actionsバンドルに含めることにした。Compiler本体はバージョン差異が大きいことが予想されるが、ECMAScript Globalの型定義ならばバージョン差異が悪い方向に働くことはないだろうという楽観的判断によるものである。

実装の話をさせてもらうと、本ツールはWebpackでビルド & トランスパイルしたJSファイルをNodeで動かしている都合上そのままでは d.tsファイルを取り回すことはできないため、あらかじめ lib.*.d.tsの記述内容およびType Referenceの依存ツリーを解析してJSON形式で出力するようなスクリプトを用意した。

pack lib.d.ts

Languaga Serviceからなんらかの lib.*.d.tsファイルにアクセスが発生する場合は、ファイル読み取り処理に介入してJSON上の値を返却するよう先述のLanguage Service Hostに手を加えることで対応している。

tweak language service host

なかなか 泥くs 味わい深いロジックが散らばっているのでそのあたりの実装に興味がある数奇者な御仁は本ツールのソースコードを参照されたい。

端的に言って、二度とやりたくない。

キャッシュによるRead高速化

この手の解析ツールを実装する際にパフォーマンスのボトルネックとなるのはやはりIOである。 npmエコシステムの特殊さも合間って往往にしてプロジェクトのファイル依存ツリーは膨大なサイズとなるため、都度ファイルシステムにアクセスされてしまう設計ではなかなかにしんどい。

そのため今回はメモリ上の Mapオブジェクトをキャッシュレジストリとすることで、Read処理の高速化を図った。 比較的導入が簡単で即効性があるためCompiler APIを触る際にはほぼ必須なプラクティスであるが、ソフトウェアの性質によりASTに破壊的変更が発生する場合などはSnapshotのrevision管理がシビアなので要注意。

まとめ

近頃、自分の観測範囲でもTS Compiler APIのユースケースが徐々に増えてきたように思える。 まだまだドキュメントが整備されていなかったり、問題にハマった際に泣きつく場所がなかったりと課題はあるものの、使いこなせればTypeScriptが持つ強力な型システムの恩恵を様々な場で受けることができるのでユーザーが広まっていくといいなぁ

P.S. TypeScriptにcustom reporterを渡せればわざわざGitHub Actionsを作る必要もなかった気がする