Upgrade to Pro — share decks privately, control downloads, hide ads and more …

レガシープロジェクトで メタプログラミングを使った PHPStan静的解析Level上げ - PHP Conference 2020

レガシープロジェクトで メタプログラミングを使った PHPStan静的解析Level上げ - PHP Conference 2020

半年くらい前にPHPStan静的解析をはじめました。

徐々に対象ファイルを増やし、現在では2000超のファイルをスキャンしています。
level0(不明なclass、関数の参照などの基本的なチェック)から段階的に厳しくして、level2(未知の全ての関数のチェック、PHPDocの検証)に上がります。

レガシープロジェクトにありがちな名前空間がない、PHPDocがないといった問題を、nikic/PHP-Parserを武器に乗り越えてきました。

PHPDocで補いきれない部分は、自作のYii1フレームワーク用のPHPStan拡張で解析しています。

レガシープロジェクトで、静的解析を進めてきた方法を紹介します。

Cf263df1d0a4cce435490e093e1a33ee?s=128

taki komiyama

December 09, 2020
Tweet

Transcript

  1. レガシープロジェクトで メタプログラミングを使った PHPStan静的解析Level上げ 小宮山 太樹 エンジニア

  2. 小宮山 太樹 ( こみやま たき ) 弁護士ドットコム株式会社 在籍2年半 PHP歴 4年ぐらい

    コンテナアプリ開発が得意 Code for JapanでCI/CD & AWSまわりで活動 Bengo4.com 自己紹介
  3. 弁護士ドットコムというサービス • 一般市民と弁護士を結びつけ、 お悩み解決を目指す • 2005年サービス開始 • 約900万セッション/月 • 約300件の法律相談/日

    Bengo4.com
  4. 目次 1. 技術負債とレビューコスト 2. PHPStanの導入 3. PHPStanとの付き合い方 4. 技術負債とメタプログラミングの実践 5.

    開発効率の改善 Bengo4.com
  5. 技術負債とコードレビュー Bengo4.com 1

  6. 技術負債 • 事業成長優先で、リファクタリングできなかった時期があった ◦ 昔のコードが、無法地帯で予想外の書き方 • error_reportingをE_ALLにすると、Noticeエラーが大量発生 • PHP7.3だが、フレームワークはYii1 •

    ユニットテストが少なく、カバレッジが低い Bengo4.com
  7. レビューコストを削減したい • 約6000のPHPファイルと約55万行の大量のコード • 大規模にリファクタリングしたい ◦ 動作確認しても、ぬぐいきれない不安 ◦ 理不尽なバグの記憶... •

    新しいコードは、厳しいコーディング規約を適用したい Bengo4.com
  8. 静的解析で攻める • コード量が多くても、機械なので同じ精度で解析可能 • テストコードをかく必要がない • CIで実行できる ◦ レビュー前に実行すれば、事前にミスに気付ける Bengo4.com

  9. PHPStanの導入 Bengo4.com 2

  10. PHPStan (PHP Static Analysis Tool) • slovemat/coding-standardの開発者が作成 • PHPDocがなくても解析可能 •

    composer autoloadなど一部PHPを実行するので解析が早い • Level 0 ~ 8まで段階的なルール設定 • エラー無視設定が柔軟 Bengo4.com
  11. PHPStanが検知できるエラー、検知できないエラー • 検知可能なエラー ◦ 未定義の関数、class、プロパティ ◦ PHPDocの構文チェック ◦ 型チェック、分岐のチェック、デッドコードのチェック •

    検知不可能なエラー(無限ループ...) Bengo4.com
  12. Bengo4.com 2067/2067 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% ------ ----------------------------------------------------------------------- Line models/Category.php ------ -----------------------------------------------------------------------

    656 PHPDoc tag @param has invalid value ($actionId): Unexpected token "$actionId", expected type at offset 123 656 PHPDoc tag @param has invalid value ($additionalParams): Unexpected token "$additionalParams", expected type at offset 147 656 PHPDoc tag @param has invalid value ($controllerId): Unexpected token "$controllerId", expected type at offset 95 ------ ----------------------------------------------------------------------- [ERROR] Found 3 errors PHPStan エラー
  13. PHPStan エラー無視設定 • エラー文に、正規表現が使える • 対象ファイル、発生数が指定可能 • ベースライン機能(エラー無視リストを自動生成) ◦ ファイルとエラー文、発生数を記録

    ◦ 新しく追加したコードだけ綺麗に書くように制限できる Bengo4.com
  14. Bengo4.com - message: "#^Cannot access property \\$email on array\\.$#" count:

    2 path: ../../components/OpenID.php - message: "#^Access to an undefined property CAction\\:\\:\\$\.*\\.$#" path: ../../components/helpers/ReplaceStringToLinkHelper.php PHPStan エラー無視設定
  15. PHPStan導入戦略 • 一番緩いルールのLevel 0から始める • できる限りエラーは修正 • エラーは0件がデフォルト • 汎用性重視、PHPDocで静的解析を補助

    • スキャンできるファイルから始める Bengo4.com
  16. Level 0から始めた理由 • Yii1用の拡張がなく、自作が必要 ◦ Level2以上でフレームワークまわりで、誤検知エラー多発 • 静的解析できるファイルが少なく、対象を増やす事を優先 ◦ 同名のclassが大量にあり、読み込めない

    • 実験的な試みなので、スモールスタート Bengo4.com
  17. PHPStanとの付き合い方 Bengo4.com 3

  18. PHPStanの型推論, PHPDoc構文チェック • PHPDocを優先して理解する ◦ PHPDocの型省略は不可 ◦ 基本的な型、記法は全て使える ◦ nullableやユニオン記法etc..

    • PHPDocと実装の差も検知 Bengo4.com /** * @param int $id * @return static|null */ public function create($id) ....... * @return string|false
  19. PHPStanの継承時の型推論の限界 • 継承元のclassの関数呼び出し ◦ 戻り値が、self or staticだと ◦ 継承元classのままでエラー •

    PHPDoc等で補完してあげる必要あり Bengo4.com * @return static ....... public function find(): self ....... /** @var Model[] $result */ $result = $this->create();
  20. PHPStanでviewを解析 • class定義がなくても解析できる • Controllerから変数を渡している ◦ 未定義変数エラー ◦ PHPDocを書けば解析可能 •

    PHPStan v0.12.30以上 Bengo4.com <?php /** @var Yii\web\View $this */ /** @var ActiveForm $form */ <div> <p><?= $this->title; ?></p>
  21. PHPStanの拡張 • PHPStanの動作を拡張できる ◦ 独自のコードで特定の状況をチェックするカスタムルール ◦ マジックメソッドの関数やプロパティの探索など • 汎用性を考えれば、基本はPHPDoc、ラップclassで対応するべき Bengo4.com

  22. 最小限のYii1用PHPStan拡張を作る • PHPStan Level 2から全ての関数、プロパティが解析される ◦ Level 3以降も、多少拡張の改修は必要そう • Yii1フレームワーク依存の戻り値の型特定ルール

    ◦ アプリケーションclassの参照時 ◦ シングルトンインスタンス取得関数呼び出し時 Bengo4.com
  23. 技術負債とメタプログラミングの実践 Bengo4.com 4

  24. メタプログラミングの武器 Bengo4.com

  25. nikic/PHP-Parser • PHP8の開発者が作成 • PHPだけで動く、PHPのためのパーサー ◦ PHPファイルをAST(抽象構文木)に解体して、組み直せる ◦ 空白、改行も維持できる •

    PHPStanでも使われている大御所 Bengo4.com
  26. Bengo4.com <?php function test($foo) { var_dump($foo); } AST(抽象構文木)変換 array( 0:

    Stmt_Function( byRef: false name: Identifier( name: test ) params: array( 0: Param( type: null
  27. PHP-Parserは細かい調整に向かない • PHP-CS-Fixer, PhpStorm ◦ コードスタイルの修正 ◦ useでのエイリアスの並び替え ◦ 絶対パス参照の調整

    • 正規表現の置換も併用 Bengo4.com
  28. 事例1 名前空間を機械的に付与 Bengo4.com

  29. 技術負債: 名前空間のない大量の同名のclass • 事象 ◦ PHPStan実行失敗。scanDirectoriesで、同名classエラー発生 • 原因 ◦ 同名classが600ファイル以上(indexActionが、100以上)

    ◦ Yii1フレームワーク内部で、名前空間なしでclassをloadしていた Bengo4.com
  30. メタプログラミング: 名前空間を機械的に付与 • 名前空間を付与しclass名の重複排除、PHPStanを実行可能に • PHP-Parserで、名前空間を付与してPHPファイルに戻す ◦ 名前空間が既についているclassはパス • 呼び出し元の特殊記法を正規表現で置換

    Bengo4.com
  31. Bengo4.com class indexAction extends CAction { public function run() {

    $userId = Yii::app()->user->getId(); Before
  32. Bengo4.com namespace application\controllers\prefecture; use CAction; use Yii; class indexAction extends

    CAction { public function run() { $userId = Yii::app()->user->getId(); After
  33. 事例2 ActiveRecordのPHPDocを機械生成 Bengo4.com

  34. 技術負債: Active RecordのPHPDocがない • 事象 ◦ PHPStanのLevel1から、Active Recordで未定義エラー大量発生 • 原因

    ◦ Active Recordのカラムデータはマジックメソッド参照 ◦ プロパティ等の実態が定義されていない、PHPDocもない Bengo4.com
  35. メタプログラミング: Active RecordのPHPDocを機械生成 • Active RecordのPHPDocを生成し、PHPStanの解析を可能にする • PHP-Parserで、特定の関数、DBの定義を読み取る ◦ 取得データを加工して、PHPDocを生成

    ◦ Active Recordを継承していないclassはパス Bengo4.com
  36. Bengo4.com /** * class News */ class News extends CActiveRecord

    { public function relations() { return [ 'NewsTopImage' => ... Before
  37. Bengo4.com /** * class News * * @property int $id

    * @property string|null $name * @property NewsTopImage|null $HeadImage * @property NewsTopImage[] $Image */ class News extends CActiveRecord After
  38. 開発効率の改善 Bengo4.com 5

  39. 2000ファイル超でLevel 2達成 • Level 0 ~ 1(エラー 約250件) ◦ Active

    RecordのPHPDoc付与、名前空間の付与 etc... ◦ エラーを99%修正、4個だけエラー無視リストへ • Level 2(エラー 約2500件) ◦ PHPStan拡張の作成、PSR違反のPHPDocの修正 etc.. ◦ エラーを約70%修正、残りはベースラインで無視リストへ Bengo4.com
  40. 改善1 レビューコストが削減 • レビュー前に、PHPStanをCIで実行 ◦ エラーが出たらCI失敗 • PHPDocのミス等の指摘がなくなった • より本質的なレビューが可能に

    Bengo4.com /** * @return string */ public function getGender() { if (!$this->gender) { return false; } return $this->gender; }
  41. 改善2 エラーの減少 • 既存のエラーの減少 ◦ Level 0 ~ 2 のエラー修正の効果

    ◦ Noticeエラー、潜在バグの減少 • 新しいエラーの減少 ◦ CIのPHPStan失敗で、 リリース前に気づけた Bengo4.com const WORD_MAX = 20; .... $newKeyword = array_merge( $addKeywords, $keyword ); if ($newKeyword > self::WORD_MAX) { $newKeyword = array_slice(
  42. 改善3 IDEでのコード補完が強化 • 下記で、コードジャンプ等が可能に ◦ マジックメソッドのPHPDoc追加 ◦ 名前空間の追加 ◦ 誤ったPHPDocの激減

    Bengo4.com * @param $id int ....... * @params string $id ....... * @method DELETE int
  43. これから • ベースラインを駆使してPHPStanのLevel 3 ~を目指す ◦ PHPStan拡張の開発も継続 ◦ viewを含め対象を拡大 •

    並行でユニットテストも増やす • 型づけの強化 • フレームワークの載せ替えなど、大規模な改修につなげる Bengo4.com
  44. まとめ • PHPStanの静的解析はとても賢い ◦ 融通が効かないところは、PHPDocでアシストしてあげる ◦ 完璧は目指さず柔軟に • 機械的に修正できるのは、メタプログラミングでリファクタリングしよう ◦

    nikic/PHP-Parser、PhpStorm等で出来ることは多い • プログラムが出来ることは、プログラムに頼る Bengo4.com
  45. Bengo4.com

  46. メタプログラミングの実践で作成したライブラリのリンク • 事例1 :名前空間を機械的に付与 ◦ koriym/spaceman ▪ https://github.com/koriym/spaceman • 事例2

    :ActiveRecordのPHPDocを機械生成 ◦ bengo4/yii-ide-helper ▪ https://github.com/bengo4/yii-ide-helper Bengo4.com
  47. 紹介したライブラリのリンク • PHPStan ◦ https://github.com/phpstan/phpstan • slevomat/coding-standard ◦ https://github.com/slevomat/coding-standard •

    PHP-Parser ◦ https://github.com/nikic/PHP-Parser Bengo4.com