見出し画像

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に導入

スクリーンショット 2020-06-24 20.39.36

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やもっと上を目指していきたいです。

追記:名前空間については、詳しくこちらにまとめました。

#弁護士ドットコム #php #静的解析 #phpstan

この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
ありがとうございます♪
22
弁護士ドットコム所属 PHP, JavaScript, Pythonエンジニア