超PHPerになろう

Enjoy PHP Programming

PHPStan 2.0リリース: Level 10とElephpant!

この記事はPHPStan開発者のOndřej Mirtesによって2024年11月11日にPHPStan Blogに書かれた記事を翻訳したものです。

phpstan.org

PHPStan 1.0は約3年前にリリースされました。プロジェクトが順調に成長していることを報告できて嬉しく思います。それ以来176回のリリースでは、新機能の実装、バグの修正、そして2.0のための基礎固めが行なわれてきました。そうです、私たちは休むことなく、栄光に甘んじることもありませんでした。

私は2.0のリリースを長い間待ち望んでいました。私たちが取り組んできた新機能を、ついに皆さんに楽しんでいただけるようになります。それらの一部は既に2年以上もアーリーアダプターの方々に楽しでいただいていたものです。

しかし、今日リリースされたのはコードと分析の変更だけではありません。PHPStanは伝説的なPHPマスコットに独自のアレンジを加え、象(elephpant)の家族に仲間入りすることになりました!

PHPStan Tシャツも、青と白、ストレートカット(メンズ)/フィットカット(レディース)で再注文できます。

これから4週間(2024年12月8日まで)注文を受け付けますので、この機会をお見逃しなく、今すぐElephpantとTシャツを注文してください! 野生の象を見られることが待ちきれません。

(訳注: 日本国内にお住まいの方向けには公式の共同購入があります)

www.phper.ninja


さて、コードの紹介に移りましょう。包括的なリリースノートは膨大で、180以上の項目からなります。そのため、PHPStan 2.0には、初めての試みとして、エンドユーザーと拡張機能の開発者それぞれにとってわかりやすいアップグレードガイドが付属しています。

2.0から好きな機能や変更点を選ぶのは本当に難しいことですが、やってみましょう:

レベル10

レベルMAX愛好家のための新しい挑戦です! 以前追加されたレベル9ではコードにmixedを使用することは全く安全ではなく、実際にはそれについてなんとかすべきだと認識されています。ただし、いくつかの盲点があり、依然としていいくつかのエラーが発生します。内部的にはcheckExplicitMixedという名前が付けられています。つまり、コード内で明示的にmixedとして型付けされた値にエラーが報告されます。

レベル6では欠落している型を追加する必要があります。チート、えーっと、不正行為、げふんげふん、ベースラインによって型定義をスキップしたり、戻り値にあまり型が書かれていないサードパーティのコードを呼び出す場合、解析中にコードに不明な型が表れる可能性があります。これらは暗黙的にmixedとして型付けされ、レベル10で検出されます。

list型

PHPの配列は実にパワフルですが、単一のデータ構造によってコンピュータサイエンスのさまざま概念を表現するため、扱いが難しいことがあります。そのため、リストのような単一の概念として扱いたい場合には配列を絞り込むのが便利です。

PHPStanが扱うリストとは、インデックスが0から始まり、隙間のない連続した整数キーを持つ配列のことです。PHPDocで表現できる多くの高度な型の一つとして追加されました。

<?php

/** @param list<int> $listOfIntegers */
public function doFoo(array $listOfIntegers): void
{
}

メモリ消費量を減らせば、より高速化されます

これまでのPHPStanは飢えた獣でした。php.iniでmemory_limitに設定された上限のメモリを使用してしまうばかりでなく、ハードウェアに割り当てられたメモリまでもを食い潰し、CIランナーにkillされてしまうまでになってしまいました。

そんな獣も、もはや前ほどに飢えることはなくなりました。何が獣を変えたのでしょうか。実行中のプログラムの内部で中で何が起こっているのかを知ることは役に立ちます。有意義なことを成し遂げるのにメモリを使うのは問題ありませんが、トータルでのメモリ使用量を削減するためには次のファイルを解析するために必要なデータを再利用し、開放する必要があります。

メモリリークデバッグするには、php-meminfo拡張モジュールを利用します。メモリのほとんどがASTノードによって占有されていることには、すぐに気付きました。PHP参照カウントという方式でメモリを管理していて、基本的にオブジェクトが参照されなくなると占有していたメモリを解放します。ところがASTノードはオブジェクトを相互に参照するので、期待通りには機能しません。

ノードのparent/previous/next属性を削除するとそれらを読み取るカスタムルールの下位互換性を損ねてしまいます。メモリを浪費してしまう参照なしでこれらのルールを機能させる方法についての記事を書きました。

オブジェクトグラフは目に見えてクリーンで、循環参照がなくなりました。

私の検証では、PHPStanは数千のファイルがある大規模なプロジェクトでメモリ消費量が約50~70%削減されました。PrestaShop 8.0コードベースの分析は、2つのCPUコアを使用したGitHub Actionsで9分かかっていたものが、3分に短縮されました。

インラインPHPDoc@varタグを検証する

インラインの@varPHPDocタグにはいくつもの問題があります。PHP開発者たちがこれを利用する理由として、主な理由は2つあります。

  • サードパーティの間違ったPHPDocを修正する。(おそらく静的解析されていない)依存関係のPHPDocには@return stringと書かれているが、実際にはnullが返される。
  • 返された型を絞り込む。関数はstringを返すが、この場合はnon-empty-stringしか返されないことがわかっている。

解析されたコードを見ても、それが実際にどちらのケースにあたるかを判断することはできません。これまでPHPStanは常に入力を信頼し、エラーの可能性を報告しませんでした。型の入力が同期しなくなり、@varはいともたやすく間違ってしまう可能性があるので、これは明らかに危険です。しかし既存のユースケースを念頭に置いて、誤検知なく何を報告できるかというアイディアを思い付きました。

PHPStan 2.0はインライン@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();

私はPHPコミュニティに @var の使用を徐々に減らしてもらいたいと考えています。条件付き戻り値型@phpstan-assertジェネリクス(日本語訳: PHPDocを使ったPHPのジェネリクス - 超PHPerになろう)、サードパーティのPHPDocを上書きするスタブファイルDynamicReturnType拡張など、コードの重複を排除するための良いプラクティスや代替手段がいくつもあります。

Impure pointsによる@phpstan-pureが本当かの確認

「純粋関数」と呼ばれるものは、同じ入力(引数およびオブジェクトの状態)に対して、常に必ず同じ値を返します。また、現在時刻や乱数生成器、ファイルの内容の読み取りやネットワーク経由のリソースアクセスなどのIOなどといった副作用がありません。

関数やメソッドのロジックが純粋ならば、@phpstan-pureとマークすると便利です。PHPStan 2.0においてはこのアノテーションが強制され、それによって内部の不純(impure)なコードをすべてエラーとして報告するようになりました。

私たちは、impure points(不純な箇所)の助けを借りてこの機能を実現しました。PHPがサポートしているあらゆる文(statement)と式(expression)について、PHPStanはそれが純粋かどうかを見分けることができます。

コードのimpure pointsを知ることによって、PHPStanがより多くのデッドコードを報告できるようにもなりました。純粋なメソッド呼び出しだけが書かれた行で、結果が使用されないコードは無意味です。副作用がなく戻り値が使用されないならば、その行は安全に削除できます。

<?php

// メソッド Foo::pureMethod() を単独の行で呼び出すのは無意味
// Call to method Foo::pureMethod() on a separate line has no effect.
$this->pureMethod();

また、voidメソッドにimpure pointがないということは、そのメソッド定義そのものが不要だということができます。

<?php

// メソッド Foo::add() は void 戻り値だが、いかなる副作用もない
// Method Foo::add() returns void but does not have any side effects.
private function add(int $a, int $b): void
{
    $c = $a + $b;
}

キャッシュの削減とディスク領域のクリーンアップ

解析の速度を低下させずにキャッシュが削減できれば、明らかに大勝利です。無効にするものが減り、心配ごとも減り、占有されるディスク領域までもが減らせます。

これまでPHPStanは関数が実装内でfunc_get_args()呼び出しをしているかによって可変長引数であるかの情報をキャッシュしていましたが、この情報は実行ごとに新たに利用できるのでキャッシュする必要がなかったことに気付いてしまいました。これによって、PHPStanがディスクに保存して読み取っていた数万のファイルが文字通り節約されます。

現在のPHPStanは、解析を高速化するための結果キャッシュだけに依存するようになりました。

この機会にディスク上の古いキャッシュ項目をクリーンアップしたので、PHPStan 2.0を初めて実行すると、これまで占有していたディスク領域がいくらか解放されることがあります。私がPHPStanをテストしている独自のプロジェクトでは約1GBもの領域が解放されました。

PHPStanの未来

3年前にも言ったことですが、また言いましょう。PHPStanの未来は明るいです。PHPStan ProGitHub Sponsorsのお蔭で、これが私のフルタイムの仕事になり、収益はおよそ50:50です。

私は自分の仕事が大好きで、PHPも愛しているので計画を止めるつもりはありません。貢献者たちは非常に活発で、なくてはならない存在です。PHPStan拡張のエコシステムも同じように繁栄しています。

これから挑戦したい野心的でクレイジーなアイディアは山ほどあります。しかし、PHPStan 2.0のリリースが落ち着いたら最初に取り組むのは、PHP 8.4の新機能をサポートすることです。2024年中に対応予定ですので、ご期待ください。


PHPStanが好きで、毎日使用していますか? GitHub SponsorsでPHPStanのさらなる開発をサポートし、PHPStan Proに登録してください。本当にありがとう!

(翻訳ここまで)

訳者によるまとめ

PHPStan 1.xの3年間、長かったような短かかったような… この記事で挙げられている改善の一つ一つはbleeding Edge(人柱版)として既に利用可能だったものが、ようやく一般公開されたと思うと、感慨深い気持ちがありますね。

最後に触れられてるキャッシュファイルの削減の話はこの記事を訳しながら読んで気付いたのですが、業務のプロジェクトで検査してみると数万のファイルと100MBちょっとの容量が綺麗さっぱり消えてよかったですね。社内で運用してるPHPStan拡張はアップグレードマニュアルを読みながらコーディング時間は20分かからなかったくらいで作業できました。

PHPStan 2.0は非常に快適ですし、もちろん私が開発しているphpstan.elもそのまま動きます。ただ、Rectorはまだ対応できていないので一時的に依存関係から外しています。必要だったらcomposer-bin-pluginとか使えばいいでしょう。

個人的にはPHPStanの貢献者として現状8位くらいに並べたのはちょっと嬉しいですね……! PHPStanの改善はまじで取り組み甲斐があるタスクがいっぱいあって楽しいので、みんなもやってみてほしいですね!

あとは、PHPStan 1.12.7で追加されたおもしろ機能があるのですが… これについては別記事で書きますね!