5千ファイル超のレガシープロジェクトにPHPStan継続的静的解析を導入
以前、断捨離でテーブル約50個消した話で、大規模にdbまわりのリファクタリングをした話を書きました。
弁護士ドットコムのサイトは、10年以上運用されているため、5千ファイルある巨大PHPプロジェクトです。そのため、課題はいろいろあります。
弁護士ドットコム - 無料法律相談や弁護士、法律事務所の検索
https://www.bengo4.com/
今回は、PHPStan静的解析をCIに導入し、継続的なコード品質の向上を目指しました。
静的解析を導入する目的
コードベースが長年の拡張により巨大になった結果、全ての関数やclassの使用箇所を目視で把握するのは、厳しくなってました。いくら注視していても、対象が広くなるほど漏れは発生しやすくなります。
そのため、人間の目では見落としやすいバグを機械的に見つけることで、コード品質の向上に繋がると考えました。PHPStanをCIでレビュー前に回すことで早い段階でのバグの混入を防ぎ、レビュアーの負担軽減を狙いました。
静的解析ツールPHPStanとは
PHPStanは、PHP静的解析ツールの大御所です。composerなどのautoloadファイルを解釈し、一部のコードを実行することで解析の高速化を実現しています。静的解析ですが、PHPを一部実行します。実行環境は、PHP7.1以上です。
そのため、PHPDocを書いてなくても、ある程度解釈してくれます。また、無視するエラー内容の正規表現を記述できるなど、小回りが効きます。特別なphp拡張は必要ありません。
似たような静的解析のツールとして、Phanがあります。
こちらは完全に静的解析で、コードを実行しません。そのため、PHPを抽象構文木として解釈するために、ext-astのphp拡張に依存しており、実行環境PHP7.2以上が必要です。またPHPDocを手がかりにするので、どのくらい書かれているか重要です。
弊社では、最近PHP5.5から7.3へバージョンアップに成功しており、特別なphp拡張が不要で直接アプリコンテナで動かせるのは魅力的でした。また古いコードには、PHPDocが全く書かれていませんでした。これらを踏まえた結果、手軽さ、拡張性の高さ、柔軟性からPHPStanを採用しました。
下記のメルカリでの事例もあり、とても参考になりました。
最低レベルのLevel 0で導入を目指す
PHPStanでは、ルールという概念があり、どのくらい厳しくチェックするか設定することができます。下記がルールごとにチェックされる内容です。
0.基本的なチェック、不明なクラス、不明な関数、呼び出された不明な$thisメソッド、
それらのメソッドと関数に渡された引数の数が間違っている、常に未定義の変数
1.おそらく未定義の変数、未知の魔法のメソッドとプロパティ__call、__get
2.PHPDocsの検証($thisだけでなく)すべての式で不明なメソッドをチェック
3.戻り値の型、プロパティに割り当てられた型
4.基本的なデッドコードチェック。
常にfalse instanceofおよびその他の型チェック、デッドelseブランチ、戻り後の到達不能コード。等
5.メソッドと関数に渡された引数のタイプのチェック
6.タイプヒントの欠落を報告
7.部分的に間違った共用体型を報告する-共用体型の一部の型にのみ存在するメソッドを呼び出すと、レベル7がそれを報告し始めます。その他の誤った状況
8.メソッドの呼び出しとnull許容型のプロパティへのアクセスを報告する
弊社はYiiフレームワークを使っており、基本的にMVCです。
Yii のActiveRecordの特徴として、__getのマジックメソッド経由でプロパティが参照されます。そのため、level1にすると、PHPStanでは追いきれない未定義の変数エラーが大量に検知されてしまいます。
PHPStanの拡張を書くことで回避できますが、ハードルが上がります。
level 0でも、200超のエラーが検出されたので、最初はレベル0での導入を目指しました。
ファイル数が多いので、徐々にエラーを直して適応範囲を増やし、段階的にlevelをあげていく戦略を取りました。
また、viewレイヤーではcontrollerから変数を渡しており、未定義の変数参照等のエラーが多量に検知されたので、スキャン対象から一旦除外しました。
200超あるエラーをメタプログラミングで直す
PHPStanを実行すると、下記のようなエラーが大量にできます。class名と行数が書かれており、わかりやすいです。
------ ------------------------------
Line components/SalesforceApi.php [39m
------ ------------------------------
312 Undefined variable: $data
346 Undefined variable: $data
------ ------------------------------
エラーを分類して、IDEの正規表現置換等を駆使して、機械的に修正していきます。直しきれないところは、目視で対応します。
修正前と修正後で、対象のエラーが修正できているのか、新しいエラーが発生していないかPHPStanの実行結果の差分を取りながら進めました。
CIに導入
PHP拡張が必要ないので、アプリのdockerコンテナで実行できます。そのため、スムーズに組み込むことができました。
アプリのコードで、php拡張等を使っている場合、それも解析に必要なので、アプリのdockerコンテナで実行するのが楽だと思います。
ファイル数が多いので、並列実行数や、ジョブの処理ファイル数を調整して、2-3分で終わるようにしました。
レビューコストの削減、デッドコード・バグの減少
既存のエラーを修正した結果、デッドコードや、Noticeでのエラーが減少しました。
また、リファクタリングをした際に、レビュー前の静的解析で、変数や定数の削除漏れ等のバグに気付けるようになりました。レビュアーもよりロジックなどの、より本質的な内容をレビューできるようになった気がします。
その結果、以前より自信を持ってリリースできるようになりました。
PHP構文解析での自動名前空間付与
古い一部のディレクトリでは、名前空間がありません。そのため、PHPStan実行でオートロード時に、class名が衝突してスキャンできないという課題が発生しています。
しかし、技術顧問の郡山昭仁さんに協力をあおぎ、nikic/PHP-Parserをベースに、phpを解釈し自動的に名前空間を付与するライブラリkoriym/spacemanを作成してもらいました。
上記のツールで名前空間を付与、エラー修正しながら、PHPStanの適応範囲を拡大しています。PHPファイルを抽象構文木に解体、処理するのは、とても勉強になりました。
あとから、PHPStanもnikic/PHP-Parserベースで解析していると知り、驚きました。
まとめ
最低限の静的解析でも、効果はかなりあります。特に巨大なレガシープロジェクトのほうが、より品質の向上を実感できるはずです。
正規表現による置換だけでなく、PHP構文解析により名前空間を付与したことで、メタプログラミングの大きな可能性を実感しました。PHPを構文解析し、拡張することで、名前空間付与以外にも、PHPDocの自動生成やかなり自由なPHPファイルが作成できると思います。
まだまだ、スタートラインに立っただけなので、level1やもっと上を目指していきたいです。
追記:名前空間については、詳しくこちらにまとめました。