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

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

taki komiyama
December 09, 2020

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

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

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

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

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

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

taki komiyama

December 09, 2020
Tweet

More Decks by taki komiyama

Other Decks in Programming

Transcript

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

    View Slide

  2. 小宮山 太樹 ( こみやま たき )
    弁護士ドットコム株式会社 在籍2年半
    PHP歴 4年ぐらい コンテナアプリ開発が得意
    Code for JapanでCI/CD & AWSまわりで活動
    Bengo4.com
    自己紹介

    View Slide

  3. 弁護士ドットコムというサービス
    ● 一般市民と弁護士を結びつけ、
    お悩み解決を目指す
    ● 2005年サービス開始
    ● 約900万セッション/月
    ● 約300件の法律相談/日
    Bengo4.com

    View Slide

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

    View Slide

  5. 技術負債とコードレビュー
    Bengo4.com

    View Slide

  6. 技術負債
    ● 事業成長優先で、リファクタリングできなかった時期があった
    ○ 昔のコードが、無法地帯で予想外の書き方
    ● error_reportingをE_ALLにすると、Noticeエラーが大量発生
    ● PHP7.3だが、フレームワークはYii1
    ● ユニットテストが少なく、カバレッジが低い
    Bengo4.com

    View Slide

  7. レビューコストを削減したい
    ● 約6000のPHPファイルと約55万行の大量のコード
    ● 大規模にリファクタリングしたい
    ○ 動作確認しても、ぬぐいきれない不安
    ○ 理不尽なバグの記憶...
    ● 新しいコードは、厳しいコーディング規約を適用したい
    Bengo4.com

    View Slide

  8. 静的解析で攻める
    ● コード量が多くても、機械なので同じ精度で解析可能
    ● テストコードをかく必要がない
    ● CIで実行できる
    ○ レビュー前に実行すれば、事前にミスに気付ける
    Bengo4.com

    View Slide

  9. PHPStanの導入
    Bengo4.com

    View Slide

  10. PHPStan (PHP Static Analysis Tool)
    ● slovemat/coding-standardの開発者が作成
    ● PHPDocがなくても解析可能
    ● composer autoloadなど一部PHPを実行するので解析が早い
    ● Level 0 ~ 8まで段階的なルール設定
    ● エラー無視設定が柔軟
    Bengo4.com

    View Slide

  11. PHPStanが検知できるエラー、検知できないエラー
    ● 検知可能なエラー
    ○ 未定義の関数、class、プロパティ
    ○ PHPDocの構文チェック
    ○ 型チェック、分岐のチェック、デッドコードのチェック
    ● 検知不可能なエラー(無限ループ...)
    Bengo4.com

    View Slide

  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 エラー

    View Slide

  13. PHPStan エラー無視設定
    ● エラー文に、正規表現が使える
    ● 対象ファイル、発生数が指定可能
    ● ベースライン機能(エラー無視リストを自動生成)
    ○ ファイルとエラー文、発生数を記録
    ○ 新しく追加したコードだけ綺麗に書くように制限できる
    Bengo4.com

    View Slide

  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 エラー無視設定

    View Slide

  15. PHPStan導入戦略
    ● 一番緩いルールのLevel 0から始める
    ● できる限りエラーは修正
    ● エラーは0件がデフォルト
    ● 汎用性重視、PHPDocで静的解析を補助
    ● スキャンできるファイルから始める
    Bengo4.com

    View Slide

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

    View Slide

  17. PHPStanとの付き合い方
    Bengo4.com
    3

    View Slide

  18. PHPStanの型推論, PHPDoc構文チェック
    ● PHPDocを優先して理解する
    ○ PHPDocの型省略は不可
    ○ 基本的な型、記法は全て使える
    ○ nullableやユニオン記法etc..
    ● PHPDocと実装の差も検知
    Bengo4.com
    /**
    * @param int $id
    * @return static|null
    */
    public function create($id)
    .......
    * @return string|false

    View Slide

  19. PHPStanの継承時の型推論の限界
    ● 継承元のclassの関数呼び出し
    ○ 戻り値が、self or staticだと
    ○ 継承元classのままでエラー
    ● PHPDoc等で補完してあげる必要あり
    Bengo4.com
    * @return static
    .......
    public function find(): self
    .......
    /** @var Model[] $result */
    $result = $this->create();

    View Slide

  20. PHPStanでviewを解析
    ● class定義がなくても解析できる
    ● Controllerから変数を渡している
    ○ 未定義変数エラー
    ○ PHPDocを書けば解析可能
    ● PHPStan v0.12.30以上
    Bengo4.com
    /** @var Yii\web\View $this */
    /** @var ActiveForm $form */

    = $this->title; ?>

    View Slide

  21. PHPStanの拡張
    ● PHPStanの動作を拡張できる
    ○ 独自のコードで特定の状況をチェックするカスタムルール
    ○ マジックメソッドの関数やプロパティの探索など
    ● 汎用性を考えれば、基本はPHPDoc、ラップclassで対応するべき
    Bengo4.com

    View Slide

  22. 最小限のYii1用PHPStan拡張を作る
    ● PHPStan Level 2から全ての関数、プロパティが解析される
    ○ Level 3以降も、多少拡張の改修は必要そう
    ● Yii1フレームワーク依存の戻り値の型特定ルール
    ○ アプリケーションclassの参照時
    ○ シングルトンインスタンス取得関数呼び出し時
    Bengo4.com

    View Slide

  23. 技術負債とメタプログラミングの実践
    Bengo4.com
    4

    View Slide

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

    View Slide

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

    View Slide

  26. Bengo4.com
    function test($foo)
    {
    var_dump($foo);
    }
    AST(抽象構文木)変換
    array(
    0: Stmt_Function(
    byRef: false
    name: Identifier(
    name: test
    )
    params: array(
    0: Param(
    type: null

    View Slide

  27. PHP-Parserは細かい調整に向かない
    ● PHP-CS-Fixer, PhpStorm
    ○ コードスタイルの修正
    ○ useでのエイリアスの並び替え
    ○ 絶対パス参照の調整
    ● 正規表現の置換も併用
    Bengo4.com

    View Slide

  28. 事例1
    名前空間を機械的に付与
    Bengo4.com

    View Slide

  29. 技術負債: 名前空間のない大量の同名のclass
    ● 事象
    ○ PHPStan実行失敗。scanDirectoriesで、同名classエラー発生
    ● 原因
    ○ 同名classが600ファイル以上(indexActionが、100以上)
    ○ Yii1フレームワーク内部で、名前空間なしでclassをloadしていた
    Bengo4.com

    View Slide

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

    View Slide

  31. Bengo4.com
    class indexAction extends CAction
    {
    public function run()
    {
    $userId = Yii::app()->user->getId();
    Before

    View Slide

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

    View Slide

  33. 事例2
    ActiveRecordのPHPDocを機械生成
    Bengo4.com

    View Slide

  34. 技術負債: Active RecordのPHPDocがない
    ● 事象
    ○ PHPStanのLevel1から、Active Recordで未定義エラー大量発生
    ● 原因
    ○ Active Recordのカラムデータはマジックメソッド参照
    ○ プロパティ等の実態が定義されていない、PHPDocもない
    Bengo4.com

    View Slide

  35. メタプログラミング: Active RecordのPHPDocを機械生成
    ● Active RecordのPHPDocを生成し、PHPStanの解析を可能にする
    ● PHP-Parserで、特定の関数、DBの定義を読み取る
    ○ 取得データを加工して、PHPDocを生成
    ○ Active Recordを継承していないclassはパス
    Bengo4.com

    View Slide

  36. Bengo4.com
    /**
    * class News
    */
    class News extends CActiveRecord
    {
    public function relations()
    {
    return [
    'NewsTopImage' => ...
    Before

    View Slide

  37. Bengo4.com
    /**
    * class News
    *
    * @property int $id
    * @property string|null $name
    * @property NewsTopImage|null $HeadImage
    * @property NewsTopImage[] $Image
    */
    class News extends CActiveRecord
    After

    View Slide

  38. 開発効率の改善
    Bengo4.com
    5

    View Slide

  39. 2000ファイル超でLevel 2達成
    ● Level 0 ~ 1(エラー 約250件)
    ○ Active RecordのPHPDoc付与、名前空間の付与 etc...
    ○ エラーを99%修正、4個だけエラー無視リストへ
    ● Level 2(エラー 約2500件)
    ○ PHPStan拡張の作成、PSR違反のPHPDocの修正 etc..
    ○ エラーを約70%修正、残りはベースラインで無視リストへ
    Bengo4.com

    View Slide

  40. 改善1 レビューコストが削減
    ● レビュー前に、PHPStanをCIで実行
    ○ エラーが出たらCI失敗
    ● PHPDocのミス等の指摘がなくなった
    ● より本質的なレビューが可能に
    Bengo4.com
    /**
    * @return string
    */
    public function getGender()
    {
    if (!$this->gender) {
    return false;
    }
    return $this->gender;
    }

    View Slide

  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(

    View Slide

  42. 改善3 IDEでのコード補完が強化
    ● 下記で、コードジャンプ等が可能に
    ○ マジックメソッドのPHPDoc追加
    ○ 名前空間の追加
    ○ 誤ったPHPDocの激減
    Bengo4.com
    * @param $id int
    .......
    * @params string $id
    .......
    * @method DELETE int

    View Slide

  43. これから
    ● ベースラインを駆使してPHPStanのLevel 3 ~を目指す
    ○ PHPStan拡張の開発も継続
    ○ viewを含め対象を拡大
    ● 並行でユニットテストも増やす
    ● 型づけの強化
    ● フレームワークの載せ替えなど、大規模な改修につなげる
    Bengo4.com

    View Slide

  44. まとめ
    ● PHPStanの静的解析はとても賢い
    ○ 融通が効かないところは、PHPDocでアシストしてあげる
    ○ 完璧は目指さず柔軟に
    ● 機械的に修正できるのは、メタプログラミングでリファクタリングしよう
    ○ nikic/PHP-Parser、PhpStorm等で出来ることは多い
    ● プログラムが出来ることは、プログラムに頼る
    Bengo4.com

    View Slide

  45. Bengo4.com

    View Slide

  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

    View Slide

  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

    View Slide