Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

PHPStanの導入 Bengo4.com 2

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

PHPStanでviewを解析 ● class定義がなくても解析できる ● Controllerから変数を渡している ○ 未定義変数エラー ○ PHPDocを書けば解析可能 ● PHPStan v0.12.30以上 Bengo4.com

= $this->title; ?>

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Bengo4.com

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

開発効率の改善 Bengo4.com 5

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

改善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(

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Bengo4.com

Slide 46

Slide 46 text

メタプログラミングの実践で作成したライブラリのリンク ● 事例1 :名前空間を機械的に付与 ○ koriym/spaceman ■ https://github.com/koriym/spaceman ● 事例2 :ActiveRecordのPHPDocを機械生成 ○ bengo4/yii-ide-helper ■ https://github.com/bengo4/yii-ide-helper Bengo4.com

Slide 47

Slide 47 text

紹介したライブラリのリンク ● 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