PHPStanを利用しているPJにおけるbaselineの進化を追う

会社などでPHPStanを導入したり荒らしたりする事があるのですが、「まずは入れてみた」状態においては、baselineの記述量をなくしていくぞ!!!!という取り組みがあります。
最終的にはbaselineがなくなるのが健全な筈ですので、その解消作業の進捗やトレンドを分かり易くしておきたいのです。

そんな時に、ふと見つけたのが staabm/phpstan-baseline-analysis でした。

コレは、「baselineファイルをソースとして、分析することで、見えてくるものがあるのではないか?」という面白ツールです。
とっても素敵なアイディア!

Software Architecture Metricsを読んだ時に、「GItのデータを用いて、各種活動の発生状況や対象について分析する」といった観点が紹介されていて、なるほど〜と思った*1のですが。
「普段の活動の中で何かしらの動機をもって作成されたり修正されたデータや、あるいは”品質だ!!!管理だ!!!"という意図がなくとも生み落とされる記録」みたいなものであれば、自身の成果物や活動についての状況を示唆するはずだな〜確かに〜〜〜!という。

で、実際にphpstan-baseline-analysisは使えそう・・というか「(ignore)エラー総数だけでも追いたい、俺が欲しい!」と思ったので、セットアップしてみました。

作るものの概要

  • 集計した結果、コレまでのトレンドを示すグラフや最新状況について可視化する
    • 一目でわかるようにする
    • 情報を集約して、1箇所でわかるようにする
  • 自動で更新して、新鮮なデータが溜まるようにする

↑をこなすための制約だったり前提条件

  • 運用上、baselineファイルは単一のものではなく複数に分割されている
    • 複数のbaselineについて個別に & 全体を統合した集計結果の出力に対応させる
  • 分析対象ファイルとは別のレポジトリに置きたい
    • 自動更新を考える上で、J-SOXが〜〜みたいなことを気にしたくない
    • 「対象レポからコードを引っ張ってきて集計にかける」をやる

データは加工していますが、出力イメージはこんな感じになります↓

トップページ = 全体の(ignore)エラー数の推移と

詳細ページ = baselineファイル別のエラー数の推移

作った

集計を実行するレポジトリを github-org/phpstan-baseline-watch 、集計対象のレポジトリを github-org/nanika-no-pj という名前だとします。

github-org/phpstan-baseline-watch には、

  • 集計を実行する処理の実装
  • GitHub Actionsで、↑と絡めつつ、 github-org/nanika-no-pj をcloneしてくる && 集計を実行する
  • GItHub Pagesに可視化結果を出力する

という機能をもたせます。

github-org/nanika-no-pj は、 .phpstan-baselines に(複数の)baselineファイルを持てる構成になっています。

全体像

こんな構成になります。

.
├── .github
├── .gitignore
├── composer.json    # phpstan-baseline-analysisを取り入れる
├── composer.lock
├── nanika-no-pj-src    # 分析対象レポジトリ
├── docs    # gh-pagesの出力対象
├── main.sh    # 集計実行のロジック
├── tmp    # 集計時一時ディレクトリ
└── vendor 
$ cat .gitignore
/vendor/
/nanika-no-pj-src/
/tmp/
!/tmp/.gitkeep

集計実行のアクション

実際に集計をキックしたり、その前段階のデータ集めをする実態は、GitHub Actionsのワークフローに委ねることになります。

処理の流れは

  1. 分析対象のレポジトリを、ignore対象のパスにcloneする
  2. (github-org/phpstan-baseline-watchの方に)Composer Installを実行する
  3. 集計実行のロジックをキックする
  4. 集計結果をcommit&pushする

というものになります。

actions/checkoutは他レポのcloneは可能ですし、fetch-depthを指定することで最新のコミット以外も取得可能です。
今回は「サブディレクトリを掘って、そこにcloneする」という方法を取りましたが、submoduleなんかでも自然だと思います。
ローカルで作業する際に、submoduleではない「自由にいじり放題なディレクトリがあると楽だった〜」くらいのフワッとした理由で、このような形にしています。

集計実行のロジック

workflowファイルに直接書くにはやや煩雑になるので、シェルスクリプトを別出しします。
ざっくりした流れを掻い摘むと、

  1. 相対日時として、今日〜60日前の範囲で1日毎にイテレーションして
    1. 初日(=HEAD)の場合だけ、最新スナップショット保持用に集計結果を出力(コミット対象となる)
  2. スナップショットを取りたい日付(に1番近い)コミットをチェックアウトして
  3. 個別に分かれているbaselineファイルから、ignoreErrorsを取得してマージするPHPスクリプトの実行し、他のbaselineファイルと並列に配置する
  4. baselineが設置されているディレクトリの.phpファイル*2ごとに、集計の実行を行い、一時ディレクトリに結果を出力する
  5. グラフの生成を行い、トップページと詳細ページにmdファイルとして埋め込む

となります。

ちなみに、このスクリプトの要素要素については、殆どChatGPTさんが書いてくれました。ありがてぇ
いくつかポイントを説明していきます

  • ポイント①
    • git checkoutとかcleanを掛けまくるので、ワーキングディレクトリを対象PJのrootに変更しちゃっています
    • その代わり、集計スクリプトの場所などで混乱しにくいように、PJ(github-org/phpstan-baseline-watch)のパスを一時変数に格納して、nanika-no-pj-src 下でゴニョゴニョしている間は各種パスを絶対パスで指定するようにする
  • ポイント②
    • unstagedファイルをnanika-no-pj-srcの下で一時的に作成しているので、イテレーションの冒頭でお掃除しています
  • ポイント③
    • 過去の情報に遡る時 = 「1日」以上前の時だけ、commitを遡ってcheckoutする・・・という分岐なのですが、今思ったらコレ要らないかもですね。
      • git rev-list -n 1 --before="${day_before} days ago" HEAD ってHEADにならない?
  • ポイント④
    • 最終的に出力されるグラフにおいて、「データファイルが読み込まれた順に、グラフの原点から配置される(=左に来る)」という挙動が見受けられたので、若い日時のデータが最初に食われるように命名しています
    • glob() の結果に従った順番通りにファイルを読み込み、iteratorにappendしている感じ
  • ポイント⑤
    • phpstan-baseline-analysisのRADMEを見ると、いい感じにスナップショット作成対象の日時が反映されている・・・?と喜んだのですが、実際に動かしてみると、集計日時には集計実行時点の現在日時が入るっぽい挙動がありました
    • そのため、出力されたファイルから日時のフィールドを直接変更しています。jqが最初から入っているの有り難い

GItHub Pagesの更新

・・・については、ほぼデフォルトのまま(強いて言えばディレクトリを /docs になるように変更したり、スケジュール実行を入れたり)なので、特に言うこともないです。 GitHub上で、PJの settings > pages に行って Build and deploymentGitHub Actions にしてあげれば、雛形を出してくれます。

やった!できたねぇ〜

君も鬼になって、baselineをゴリゴリに削っていこう!

*1:変更頻度が高いファイル(コミットが集中しているファイル)は優先的にテストを書くべき・変更容易性を高めるためのリファクタをすべきだ〜そもそも責務を持ち過ぎかもなので分割するべきだ〜〜とか、そういった類のものですね。

*2:baselineファイルをphpで作成しています