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

巨大なモノリスの静的解析をレベルMaxにする方法

Fa9a512d547f66e1733ef8c9fcbfd009?s=47 atsu.kg
October 02, 2021

 巨大なモノリスの静的解析をレベルMaxにする方法

普段開発しているコードベースでは PHPStan で静的解析をしているものの Lev.1 に留まっており、レベル上げをしようとすると大量のファイルがあって膨大な数のエラーが出てしまい手付かずの状態でした。静的解析が弱い分、ユニットテストや手動テストを主にして検証を行っていますが、手動テストの終盤で型エラーが起きてやり直しになるなど、非効率なことが起きていました。

その状況を改善すべく、モジュール毎に静的解析レベルを設定することで独立したメンテナンスを可能にし、比較的新しいモジュールからレベル上げをしていきました。
本セッションではその取り組みやつまずいたポイント等について紹介し、これから静的解析を強化していく方の参考になれば幸いです。

・解析対象と実行方法の整理
・レベル別静的解析の恩恵
・Laravel IDE Helper の問題点とその対応
・レベルを上げてからのコードの書き味

ide-helper で外部キー有無を考慮してPHPDocを生成する: https://github.com/barryvdh/laravel-ide-helper/pull/1231

Fa9a512d547f66e1733ef8c9fcbfd009?s=128

atsu.kg

October 02, 2021
Tweet

Transcript

  1. 巨大なモノリスの静的解析を
 レベルMaxにする方法
 2021/10/03


  2. 自己紹介
 • 古賀 敦士
 • 株式会社ホワイトプラス所属
 • Webアプリケーション開発、PHP基盤改善
 • メインはPHP, 時々Go,

    たまにTypeScript

  3. 
 ネット型 宅配クリーニング


  4. 今日話すこと
 1. PHPStan静的解析の基盤を整える
 2. 静的解析レベルをMAXに上げる


  5. 今日話すこと
 1. PHPStan静的解析の基盤を整える
 2. 静的解析レベルをMAXに上げる


  6. LenetのPHPコードベース
 PHP 7.4
 Laravel 6.x
 モジュラーモノリス
 10年以上稼働
 (レガシーコード多め)


  7. 静的解析(PHPStan)の概要
 PHPStan: 0.12.25(最新: 0.12.99)
 larastan: 0.5.7(最新: 0.7.12)
 解析対象パス: app, controllers,

    lib
 解析レベル: Lev.1 

  8. PHPStanレベル別主な検査内容
 0: 未知のクラス・関数、 thisで呼ばれる未知のメソッド、メソッドや関数に渡される引数の数 
 1: 未定義の変数、未知のマジックメソッド、__call および __get を持つクラスのプロパティ

    
 2: 未知のメソッド、PHPDocバリデーション 
 3: 戻り値の型、プロパティに割り当てられた型 
 4: デッドコードチェック 
 5: メソッドや関数に渡される引数の型 
 6: 欠落した型宣言
 7: 部分的に間違った直和型の検査 
 8: nullable型のメソッド呼び出しやプロパティアクセス 

  9. LenetのPHPStan適用範囲
 0: 未知のクラス・関数、 thisで呼ばれる未知のメソッド、メソッドや関数に渡される引数の数 
 1: 未定義の変数、未知のマジックメソッド、__call および __get を持つクラスのプロパティ

    
 2: 未知のメソッド、PHPDocバリデーション 
 3: 戻り値の型、プロパティに割り当てられた型 
 4: デッドコードチェック 
 5: メソッドや関数に渡される引数の型 
 6: 欠落した型宣言
 7: 部分的に間違った直和型の検査 
 8: nullable型のメソッド呼び出しやプロパティアクセス 
 ごく一部の検査しかされていない

  10. ・PHPStan静的解析
 ・ユニットテスト
 ・コードレビュー
 ・手動テスト
 Lenetのコード検証方法
 頑張る
 弱い


  11. ・PHPStan静的解析
 ・ユニットテスト
 ・コードレビュー
 ・手動テスト
 Lenetのコード検証方法
 頑張る
 弱い
 つらい
 漏れる


  12. 静的解析レベルを上げる💪


  13. Lev.1 -> Lev.2 に上げると...
 色んな場所で大量にエラーが出る → 一度に直すには大変 ×
 ×
 ×


    ×
 ×
 ×

  14. Lev.1 -> Lev.2 に上げると...
 モジュール単位で担当チームが存在 → 調整が必要 色んな場所で大量にエラーが出る → 一度に直すには大変

    ×
 ×
 ×
 ×
 ×
 ×

  15. Lev.1 -> Lev.2 に上げると...
 モジュール単位で担当チームが存在 → 調整が必要 普段触らないところは修正しづらい 色んな場所で大量にエラーが出る →

    一度に直すには大変 ×
 ×
 ×
 ×
 ×
 ×

  16. Lev.1 -> Lev.2 に上げると...
 モジュール単位で担当チームが存在 → 調整が必要 普段触らないところは修正しづらい 色んな場所で大量にエラーが出る →

    一度に直すには大変 ×
 ×
 ×
 ×
 ×
 ×
 レベルを上げられない
  17. Lev.1 -> Lev.MAX に上げると...
 集中突破できない😦

  18. 放置している間も負債は増える


  19. 他と同じくLev.1にしなければならない
 新しいモジュールも同じレベル
 New


  20. 何とかしたい!


  21. 色んなモジュールでエラー発生
 ↓
 エラー量が多い
 チーム間調整が必要
 知識が乏しいところは修正しづらい
 ↓
 レベルを上げられない
 レベル上げの課題整理
 ×
 ×


    ×
 ×
 Lev.1 -> 2

  22. 特定モジュールでのみエラー発生
 ↓
 エラー数が抑えられる
 チーム間調整不要
 得意・必要な領域から対応
 ↓
 レベル上げに着手できる!
 もしモジュール単位でレベル上げできれば
 ×
 Lev.1

    -> 2

  23. モジュール(ディレクトリ)毎に
 静的解析レベルを切り替えるには?


  24. 1回のPHPStan 実行では1つのレベルしか設定できない仕様
 複数レベルで実行するには複数回実行しなければならない
 
 Issues: https://github.com/phpstan/phpstan/issues/705


  25. 設定ファイルを分割する案
 PHPStanをレベルの数だけ実行
 phpstan analyse -c phpstan-level1.neon 
 phpstan analyse -c

    phpstan-level2.neon 
 共通設定
 phpstan-level1.neon
 level: 1
 paths: lib/Lenet/User
 includes
 レベル別設定
 phpstan-level2.neon
 level: 2
 paths: lib/Lenet/Cleaning
 phpstan.neon

  26. 共通設定
 phpstan-level1.neon
 level: 1
 paths: lib/Lenet/User
 includes
 レベル別設定
 PHPStanをレベルの数だけ実行
 phpstan-level2.neon


    level: 2
 paths: lib/Lenet/Cleaning
 phpstan.neon
 phpstan analyse -c phpstan-level1.neon 
 phpstan analyse -c phpstan-level2.neon 
 👍 シンプル
 👎 masterとの差分実行ができない
 不採用 設定ファイルを分割する案

  27. 差分実行の仕組み
 lib/Finance/File1.php
 lib/Lenet/Cleaning/File1.php
 - level: 1
 paths: lib/Finance
 - level:

    2
 paths: lib/Lenet/Cleaning
 照合ロジック
 Lev.1: lib/Finance/File1.php
 Lev.2: lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Finance/File1.php 
 phpstan analyse -c phpstan.neon --level=2 lib/Lenet/Cleaning/File1.php 
 差分
 対応表
 git diff でmasterと開発ブランチの 差分ファイルを出力

  28. 差分実行コマンドを作成
 lib/Finance/File1.php
 lib/Lenet/Cleaning/File1.php
 - level: 1
 paths: lib/Finance
 - level:

    2
 paths: lib/Lenet/Cleaning
 照合ロジック
 Lev.1: lib/Finance/File1.php
 Lev.2: lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Finance/File1.php 
 phpstan analyse -c phpstan.neon --level=2 lib/Lenet/Cleaning/File1.php 
 対応表
 PHPStanClient
 (artisan phpstan-client --diff)
 差分

  29. (artisan phpstan-client)
 全ファイル静的解析実行時
 - level: 1
 paths: lib/Finance
 - level:

    2
 paths: lib/Lenet/Cleaning
 phpstan analyse -c phpstan.neon --level=1 lib/Finance
 phpstan analyse -c phpstan.neon --level=2 lib/Lenet/Cleaning
 対応表
 対応表を読み込んで レベル分実行する
 PHPStanClient

  30. 新しいモジュールを切った時はどうなる?


  31. 新しいモジュールの設定
 lib/Lenet/Alliance/File1.php
 lib/Lenet/Cleaning/File1.php
 - level: 2
 paths: lib/Lenet/Cleaning
 - level:

    8
 paths: lib/Lenet/Alliance
 照合ロジック
 Lev.2: lib/Lenet/Cleaning/File1.php 
 Lev.8: lib/Lenet/Alliance/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=8 lib/Lenet/Alliance/File1.php 
 対応表
 追加しないといけない 
 PHPStanClient
 (artisan phpstan-client --diff)
 差分

  32. 新しいモジュールの設定
 - level: 2
 paths: lib/Lenet/Cleaning
 - level: 8
 paths:

    lib/Lenet/Alliance
 照合ロジック
 Lev.2: lib/Lenet/Cleaning/File1.php 
 Lev.8: lib/Lenet/Alliance/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=8 lib/Lenet/Alliance/File1.php 
 対応表
 追加しないといけない 
 漏れそう 設定知識が必要 PHPStanClient
 (artisan phpstan-client --diff)
 差分
 lib/Lenet/Alliance/File1.php
 lib/Lenet/Cleaning/File1.php

  33. 業務モジュールはデフォルトでLev.MAX
 - level: 2
 paths: lib/Lenet/Cleaning
 - level: 8
 paths:

    lib
 照合ロジック
 Lev.2: lib/Lenet/Cleaning/File1.php 
 Lev.8: lib/Lenet/Alliance/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=8 lib/Lenet/Alliance/File1.php 
 対応表
 複数のレベルが
 ヒットしたら
 低い方を適用
 PHPStanClient
 (artisan phpstan-client --diff)
 差分
 lib/Lenet/Alliance/File1.php
 lib/Lenet/Cleaning/File1.php

  34. 新しいモジュールのデフォルトはLev.MAX
 - level: 2
 paths: lib/Lenet/Cleaning
 - level: 8
 paths:

    lib
 照合ロジック
 Lev.2: lib/Lenet/Cleaning/File1.php 
 Lev.8: lib/Lenet/Alliance/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=8 lib/Lenet/Alliance/File1.php 
 対応表
 複数のレベルが
 ヒットしたら
 低い方を適用
 PHPStanClient
 (artisan phpstan-client --diff)
 設定不要 開発に集中できる 差分
 lib/Lenet/Alliance/File1.php
 lib/Lenet/Cleaning/File1.php

  35. これまでのまとめ
 解析対象パスとレベルの紐付けを共通設定から切り出し、
 照合ロジックを持った解析実行コマンドを作成し実行する
 モジュール毎に設定したレベルで静的解析可能になり、
 独立してレベル上げできるようになった🙌


  36. けど本当にレベルMAXまで到達できるか?


  37. 今日話すこと
 1. PHPStan静的解析の基盤を整える
 2. 静的解析レベルをMAXに上げる


  38. まずはライブラリバージョンアップ
 PHPStan: 0.12.25 -> 0.12.90
 larastan: 0.5.7 -> 0.7.8
 これをやらないと既バグを踏む可能性がある


    当時の最新 実際に踏みました󰢧

  39. 0: 未知のクラス・関数、 thisで呼ばれる未知のメソッド、メソッドや関数に渡される引数の数 
 1: 未定義の変数、未知のマジックメソッド、__call および __get を持つクラスのプロパティ 


    2: 未知のメソッド、PHPDocバリデーション 
 3: 戻り値の型、プロパティに割り当てられた型 
 4: デッドコードチェック 
 5: メソッドや関数に渡される引数の型 
 6: 欠落した型宣言
 7: 部分的に間違った直和型の検査 
 8: nullable型のメソッド呼び出しやプロパティアクセス 
 レベル上げのロードマップ

  40. 0: 未知のクラス・関数、 thisで呼ばれる未知のメソッド、メソッドや関数に渡される引数の数 
 1: 未定義の変数、未知のマジックメソッド、__call および __get を持つクラスのプロパティ 


    2: 未知のメソッド、PHPDocバリデーション 
 3: 戻り値の型、プロパティに割り当てられた型 
 4: デッドコードチェック 
 5: メソッドや関数に渡される引数の型 
 6: 欠落した型宣言
 7: 部分的に間違った直和型の検査 
 8: nullable型のメソッド呼び出しやプロパティアクセス 
 Lenet でのレベル上げのポイント
 
 

  41. Lev.4
 デッドコードチェック


  42. デッドコードチェックとは


  43. always false デッドコードチェックとは


  44. デッドコードを 削除 デッドコードチェックとは


  45. やっかいなケースに遭遇


  46. Eloquentで所有元に
 外部キーなしでアクセスする


  47. 所有元に外部キー無しでアクセス
 外部キー無き参照
 存在チェックが必要


  48. 所有元に外部キー無しでアクセス
 外部キー無き参照
 error: プロパティが存在しない


  49. laravel-ide-helper でPHPDocを付与


  50. Eloquentで所有元にアクセスする
 追加された


  51. Eloquentで所有元にアクセスする
 nullable ではない error: always false
 (デッドコード判定)


  52. Eloquentで所有元にアクセスする
 外部キーがない場合は User|null であるべき → ide-helper の課題 error: always false


    (デッドコード判定)

  53. laravel-ide-helperを修正しました
 https://github.com/barryvdh/laravel-ide-helper/pull/1231 


  54. 修正バージョンは未リリース(2021/10/1 時点)
 暫定的にide-helperを直接拡張し修正を施して対応


  55. Eloquentで所有元にアクセスする
 修正された!

  56. 同じ問題に遭遇した場合は、
 ・ ide-helper の修正バージョンリリース後に取り込む
  or
 ・ide-helper を拡張し修正する
 の対応を検討してみてください


  57. Lev.6
 欠落した型宣言


  58. 型宣言の強制


  59. 型宣言の強制


  60. プロパティ型宣言は PHP7.4 で導入
 未対応のコードが多い
 型宣言の強制


  61. 宣言漏れ
 プロパティ型宣言は PHP7.4 で導入
 未対応のコードが多い
 型宣言の強制


  62. プロパティ型宣言は PHP7.4 で導入
 未対応のコードが多い
 宣言漏れ
 全部で 731 errors
 型宣言の強制


  63. 全部で 731 errors
 全てに型を導入する には時間がかかる
 プロパティ型宣言は PHP7.4 で導入
 未対応のコードが多い
 宣言漏れ


    型宣言の強制

  64. Baseline で回避
 全てに型を導入する には時間がかかる
 プロパティ型宣言は PHP7.4 で導入
 未対応のコードが多い
 宣言漏れ
 全部で

    731 errors
 型宣言の強制

  65. Baseline とは
 検出したエラーの一覧をファイルに出力し
 それを読み込むことにより
 エラーをまとめて無視する


  66. Baseline を使用するケース
 ・より高いレベルに上げたい
 ・PHPStanバージョンアップ
 ・カスタムルールの導入
 これらによって、数十〜数百くらいのエラーが出るが対処する時間・労力が無い場 合に最適
 今回


  67. Baseline 生成・読み込み


  68. 欠落した型宣言のチェック


  69. 欠落した型宣言のチェック
 既知のエラーとなり 報告しない

  70. レベル上げ完了!


  71. レベルMAXで運用した感想
 ・型を強制し整合性をチェックすることで一定の品質を保てるようになった
 ・検査が厳格になったことでCIエラーになる頻度が増えた
  → ローカル環境で差分実行を行って頻繁にチェック
 ・対応に悩むエラーに時々遭遇
  → 解決したものをwikiで共有
 ・Baselineで無視したエラーの扱い
  →

    継続的なリファクタで残し続けないように

  72. ご静聴ありがとうございました


  73. 付録


  74. 0: 未知のクラス・関数、 thisで呼ばれる未知のメソッド、メソッドや関数に渡される引数の数 
 1: 未定義の変数、未知のマジックメソッド、__call および __get を持つクラスのプロパティ 


    2: 未知のメソッド、PHPDocバリデーション 
 3: 戻り値の型、プロパティに割り当てられた型 
 4: デッドコードチェック 
 5: メソッドや関数に渡される引数の型 
 6: 欠落した型宣言
 7: 部分的に間違った直和型の検査 
 8: nullable型のメソッド呼び出しやプロパティアクセス 
 レベル上げのロードマップ
 
 
 
 
 
 発表で説明できなかったレベル別の ルールについて簡単に紹介します!
  75. Lev.1 -> Lev.2


  76. 未定義のメソッドチェック


  77. 未定義のメソッドチェック
 
 未定義

  78. 未定義のメソッドチェック
 
 未定義の メソッドを削除

  79. 未定義のプロパティ


  80. 未定義のプロパティ
 動的プロパティ 未定義と判定される

  81. 未定義のプロパティ
 mixed で逃げる

  82. 未定義のプロパティ
 外に切り出して total を持つクラス を戻り値型として定 義して使う

  83. PHPDoc バリデーション


  84. PHPDoc バリデーション
 型定義が無い

  85. PHPDoc バリデーション
 型を定義

  86. PHPDoc バリデーション
 型をつけられない時 はmixed

  87. Lev.2 -> Lev.3


  88. 戻り値型チェック


  89. 戻り値型チェック
 型不一致

  90. 戻り値型チェック
 型を一致させる

  91. プロパティ代入時の型チェック


  92. プロパティ代入時の型チェック
 型不一致

  93. プロパティ代入時の型チェック
 型を一致

  94. Lev.4 -> Lev.5


  95. メソッド引数の型チェック


  96. メソッド引数の型チェック
 型不一致

  97. メソッド引数の型チェック
 型を一致

  98. Lev.6 -> Lev.7


  99. 直和型のチェック


  100. 直和型のチェック


  101. 直和型のチェック
 不一致


  102. 直和型のチェック
 異常系をハンドリング

  103. Lev.7 -> Lev.8(MAX)


  104. nullable型のチェック


  105. nullable型のチェック
 nullable型の メソッド呼び出し nullable型でない

  106. nullable型のチェック