PHPはスクリプト言語なので、コンパイラ型言語と違って実行前に型チェック等の静的解析は行いません(独立したコンパイル作業を必要としないので、そのタイミングがありません)。これはデメリットというよりは特徴と捉えた方が良いと思いますが、プロジェクトの規模が拡大していくと不便な面も出てきます。そこで今回はPHPStanを使って、PHPで書かれたコードの静的解析をできるようにしてみます。
PHPStanについて
PHPStanはPHP用の静的解析ツールです。「静的」解析であり、実際に動作させずに型チェック等を実施できます。
インストール
composerを使ってインストールします。なお2021年1月現在(バージョン 0.12.66)のインストール要件としては、PHP 7.1以上となっています。PHP 7.0を使っている場合、0.9系のバージョンだと使えるようです。
$ composer require --dev phpstan/phpstan
上記を実行してインストールすると、vendor/bin
下に実行ファイルができます。試しにversionを見てみます。
$ vendor/bin/phpstan --version
PHPStan - PHP Static Analysis Tool 0.12.66
ルールレベルについて
PHPStanではチェックの厳密性をルールレベルとして指定することができます。レベルはデフォルトの0から8まで存在しています(レベル0が最も緩く、レベル8が最も厳密)。理想としてはレベル8でパスできるようにしたいですが、実際は導入時点での状況に合わせて設定する必要があると思います。レベルによる差異は公式のドキュメントに記載があります。
動かしてみる
実際に動かして確認してみます。検証に使った環境は以下です。
- PHP 8.0.1
- PHPStan 0.12.66
サンプルコード
以下が解析対象のサンプルです。各level
メソッドは末尾の数字以上のレベルを指定した場合、エラーになるようにしています。露骨すぎるとは思いますが、あくまで挙動の確認用ということでご理解ください。
<?php
declare(strict_types=1);
namespace Src;
use SplTempFileObject;
class Sample
{
public function level0(): void
{
$str = 'hoge';
unset($str);
$str .= ' and fuga'; // $str is undefined
}
public function level1(): void
{
$str = 'hoge';
if ($this->maybeTrue()) {
unset($str);
}
$str .= ' and fuga'; // $str might be undefined
}
public function level2(): void
{
$obj = new SplTempFileObject();
$obj->hoge(); // call to an undefined method
}
public function level3(): string
{
return []; // should return string
}
public function level4(): void
{
$str = 'hoge';
return;
$str .= ' and fuga'; // unreachable
}
public function level5(): void
{
$str = strtoupper([]); // expects string
}
public function level6()
{
// no return type specified
}
public function level7(): void
{
$str_or_int = $this->stringOrInt();
$str = strtoupper($str_or_int); // $str_or_int might be int
}
public function level8(): void
{
$obj = $this->objectOrNull();
$obj->getSize(); // // $obj might be null
}
private function stringOrInt(): string|int
{
return $this->maybeTrue() ? 'str' : 1;
}
private function objectOrNull(): ?SplTempFileObject
{
return $this->maybeTrue() ? new SplTempFileObject() : null;
}
private function maybeTrue(): bool
{
return mt_rand(0, 1) === 0;
}
}
なお使った環境がPHP8なのでタイプヒントでユニオンが使えていますが、タイプヒントではなくPHPDocの記載でも同じ挙動になりますので、PHP 7.4までのバージョンで使う場合は読み替えてもらえればと思います。
実行結果
実際にanalyze
コマンドで解析してみます。--level
オプションによる違いが判るかと思います。
ルールレベル0で実行
未定義の変数を使用しているlevel0
メソッドの処理でエラーが検出されます。
$ vendor/bin/phpstan analyze --level 0 src
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ --------------------------
Line Sample.php
------ --------------------------
16 Undefined variable: $str
------ --------------------------
[ERROR] Found 1 error
ルールレベル1で実行
level0
に加えて、未定義の可能性がある変数を使用しているlevel1
メソッドの処理でもエラーが検出されます。
$ vendor/bin/phpstan analyze --level 1 src
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ -------------------------------------
Line Sample.php
------ -------------------------------------
16 Undefined variable: $str
26 Variable $str might not be defined.
------ -------------------------------------
[ERROR] Found 2 errors
ルールレベル8で実行
全てのlevel
メソッドにてエラーが検出されます。予期せぬnull参照を避けたいのであれば、このレベルまで通るようにしたいですね。
$ vendor/bin/phpstan analyze --level 8 src
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ -------------------------------------------------------------------------------
Line Sample.php
------ -------------------------------------------------------------------------------
16 Undefined variable: $str
26 Variable $str might not be defined.
32 Call to an undefined method SplTempFileObject::hoge().
37 Method Src\Sample::level3() should return string but returns array.
45 Unreachable statement - code above always terminates.
50 Parameter #1 $string of function strtoupper expects string, array given.
53 Method Src\Sample::level6() has no return typehint specified.
62 Parameter #1 $string of function strtoupper expects string, int|string given.
68 Cannot call method getSize() on SplTempFileObject|null.
------ -------------------------------------------------------------------------------
[ERROR] Found 9 errors
まとめ
PHPStanを使ってPHPのコードの静的解析をしてみました。別途テストコードを書かなくてもある程度の確認をしてくれるので、typoのチェックやテストが書かれていないプロジェクト(涙)にjoinした場合には助かりそうです。
おまけ(ソースを読みたい人向け)
ルールレベルの違いが公式ドキュメントだけだと、いまいちわからなくてソースを追おうとしたのですが、Phar アーカイブされていて戸惑ってしまいました。解凍のコマンドをメモしておきます。
$ phar extract -f vendor/bin/phpstan.phar path/to/tmpdir
$ ls path/to/tmpdir
bin conf preload.php resources src stubs vendor
調べ終わった後で気が付きましたが、解凍しなくてもphpstan/phpstan-srcを見れば良かった気もします……。