この記事はPHPStan開発者のOndřej Mirtesによって2023年2月21日にPHPStan Blogに書かれた記事を翻訳したものです。
私(Ondřej)はPHPStan 1.10のアイディアを実装してリリースすることを長い間たのしみにしていました。
インラインPHPDoc @var タグを検証する
My personal mission after PHPStan 1.0 is to eradicate inline @var tags from existence. Developers reach for it as an immediate remedy for their problems but it's the worst solution ever.
— Ondřej Mirtes (@OndrejMirtes) 2021年11月9日
With @var tags you're giving up all the type safety static analysis offers. pic.twitter.com/WvRvFooce4
PHPStan 1.0以降の私の使命はインライン
@var
を根絶することです。開発者は問題の姑息的な対処として@var
に手を出しますが、これは最悪の解決策です。
@var
を使用すると型安全な静的解析が提供するすべての機能を放棄することになります。
インラインの @var
PHPDocタグには複数の問題があります。PHP開発者たちがこれを利用する理由として、主に2つのシナリオがあります。
- サードパーティの間違ったPHPDocを修正する。(おそらく静的解析されていない)依存関係のPHPDocには
@return string
と書かれているが、実際にはnull
が返される。 - 返された型を絞り込む。関数は
string
を返すが、この場合はnon-empty-string
しか返されないことがわかっている。
解析されたコードを見ても、それがどのシナリオにあたるかを実際に判断することはできません。これまでPHPStanは常に入力を信頼し、エラーの可能性を報告しませんでした。型の入力が同期しなくなり、@var
はいともたやすく間違ってしまう可能性があるので、これは明らかに危険です。しかし既存のユースケースを念頭に置いて、誤検知なく何を報告できるかというアイディアを思い付きました。
最新のリリースとbleeding edgeが有効になっているとき、PHPStanはインライン @var
のタグの型をネイティブ型宣言された型に対して検査して@var
タグでひろまった嘘を見付けます:
<?php function doFoo(): string { // ... } /** @var string|null $a */ $a = doFoo(); // PHPDocタグの @var string|null はネイティブ型宣言の string のサブタイプではない
型が null
になることはありえないため、 string|null
を許可することは無意味です。PHPStanはstring|null is not subtype of native type string
(string|null
はネイティブ型の string
のサブタイプではありません)と警告し、サブタイプのみが許可されることを暗示しています。サブタイプとは、同じかそれよりも狭い型で、つまり string
または non-empty-string
であれば大丈夫です。
デフォルトでPHPStanは以下のコードに対しては何も報告しません。
<?php /** @return string */ function doFoo() { // ... } /** @var string|null $a */ $a = doFoo();
PHPDocの @return
が間違っている可能性があり、 @var
タグでそれを補正しようとしている可能性があるためです。このシナリオも報告したい場合は reportWrongPhpDocTypeInVarTag
を有効化するか、phpstan-strict-rulesをインストールしてください。
私はPHPコミュニティに @var
の使用を徐々に減らしてもらいたいと考えています。条件付き戻り値型、@phpstan-assert
、ジェネリクス(日本語訳: PHPDocを使ったPHPのジェネリクス - 超PHPerになろう)、サードパーティのPHPDocを上書きするスタブファイル、DynamicReturnType拡張など、コードの重複を排除するための良いプラクティスや代替手段がいくつもあります。
すべてのenumケースのハンドリングを推奨する
次のコードの何が問題なのでしょうか?
<?php enum Foo { case ONE; case TWO; public function getLabel(): string { return match ($this) { self::ONE => 'One', self::TWO => 'Two', default => throw new \Exception('Unexpected case'), }; } }
PHPStan 1.9はこのコードについて何も警告しませんでしたが、以下のような問題があります:
default
節は実際には必要がない- もし
case THREE
が追加されたときPHPStanは未処理のケースを警告せず、実行時エラーに直面する
PHPStan 1.10は default
ケースの例について Match arm is unreachable because previous comparison is always true.
(直前の比較が常にtrueになるので、このマッチ節には到達しません)と報告します。enumに対しての match
で default
を書かないことで両方の問題を一度に解決できるので推奨します。
case THREE
を追加すると、PHPStanは Match expression does not handle remaining value: Foo::THREE
(match
式が残りの Foo::THREE
を処理していません)と報告するようになります。default
がまだあったら警告されません。
「always true」と到達不能コードについての変更
これまでの話題に関連する変更がいくつかあります。長い間、PHPStanは一貫性のないalways true
(常に真) とalways false
(常に偽) の条件を報告していました。それにはいくつかのロジックがあり、私はみなさんにデッドコードを残してほしくはありませんでした。
<?php function doFoo(\Exception $o): void { // 報告されない if ($o instanceof \Exception) { // このコードは常に実行される } }
接続される else
節があるとき、PHPStanは以下のように報告します
<?php function doFoo(\Exception $o): void { if ($o instanceof \Exception) { } else { // ここはデッドコード // reports: // Else branch is unreachable because previous condition is always true. // (以前の条件が常に真になるため、else分岐には到達しません) } }
いくつかのオプションを有効化するかphpstan-strict-rulesをインストールすると、instanceof
をalways true
として報告できます。
私がこのような手段をとることにしたのは、以下のような「安全」なコードを書くことを躊躇させたくなかったからです:
<?php // $foo は One|Two if ($foo instanceof One) { } elseif ($foo instanceof Two) { // PHPStan は"instanceof always true" と報告し、 "else {" と書き換えられます }
上記の match
の例と非常によく似ています。そして、これらの例を除いて多くの場合ではPHPStanが常にalways true
と報告することを期待していることにも気付きました。
bleeding edgeを有効化するか次のメジャーバージョンの全ユーザーに対して、PHPStanはデフォルトでalways true
を報告するようになります。追加のオプション設定は必要ありません。「最後の elseif
」ユースケースをサポートするために、最後のelseif
とmatch
の条件節には報告しません。この振る舞いはreportAlwaysTrueInLastCondition
を設定すると上書きできます。
到達不能な条件分岐を報告する必要はもうなくなりました。それまでの条件分岐からのalways true
エラーのおかげで、それに気付くことができるようになります。これらのルールはbleeding edgeで完全に無効化されます。
これらの変更により、enumの match
の default
を指定したときの警告はPHPStan 1.10でbleeding edgeのあるなしによって変わります。
<?php enum Foo { case ONE; case TWO; public function getLabel(): string { return match ($this) { self::ONE => 'One', self::TWO => 'Two', default => throw new \Exception('Unexpected case'), }; } }
-13 Match arm is unreachable because previous comparison is always true. +12 Match arm comparison between $this(aaa\Foo)&aaa\Foo::TWO and aaa\Foo::TWO is always true. + 💡 Remove remaining cases below this one and this error will disappear too.
PHPStanは役に立つように努めており、コマンドラインで💡絵文字の横にTips(ヒント)を表示します。これらのTipsは現在プレイグラウンドにも組み込まれています。default
節を削除すると、PHPStanは(enumに新たなcaseが追加されるまで)このコードに文句を言わなくなります。
私の型が受け入れられないのはどうして?
PHPStanが何について文句をつけているのかは、必ずしも自明ではありません。型安全性はもっとも重要であり、潜在的な実行時エラーが起こる余地を残したくありません。開発者は特定のチェックによって回避しようとしている状況や、なぜそれを修正する必要があるのかを理解していない可能性があります。
PHPStan 1.10には直感的ではないシナリオで役立つ文脈に沿ったTipが含まれています。私のお気に入りは次のとおりです。
instanceof *Type
の非推奨化
PHPStan 1.10ではカスタムルールやその他の拡張機能の理想的とは言えないコードパターンの非推奨化も含まれています。
これらについては、2週間前に別の記事を書きました。
PHPStanが好きで、毎日使用していますか?
GitHub Sponsorsで PHPStan のさらなる開発をサポートすることを検討してください。本当にありがとう!
訳者によるまとめ
このバージョンでも数多くの変更や改善が含まれていますが、この記事で述べられているように野放図な@var
に掣肘を加える機能が導入されたことは大きいでしょう。@var
によって型安全性が壊されるという問題は、TypeScriptのas
について日本語でも敗北者のTypeScriptのような記事で指摘されています。
私がPHPerKaigi 2023のパンフレットに寄稿した「PHPStanクイックガイド2023」においても@var
に頼らない型付けをするように構成しました。ただ内容的にかなり詰め込んでいるので、型付けの方法について別に記事を書いていきたいところです。
「instanceof *Type
の非推奨化」の問題はPHPStanの内部実装を追っている人以外にはぴんとこないかもしれませんが、1.9のリリース記事に書かれていた型リファクタリングの延長線上の話題なので興味があればぜひ読んでみてください。
PHPStanはアップデートする度に進化していくので、軽率にアップデートして、できればbleeding edgeも有効化して使ってみましょう。