menu-icon

PHPStanで静的解析をする

PHPはスクリプト言語なので、コンパイラ型言語と違って実行前に型チェック等の静的解析は行いません(独立したコンパイル作業を必要としないので、そのタイミングがありません)。これはデメリットというよりは特徴と捉えた方が良いと思いますが、プロジェクトの規模が拡大していくと不便な面も出てきます。そこで今回はPHPStanを使って、PHPで書かれたコードの静的解析をできるようにしてみます。

PHPStanについて

PHPStanはPHP用の静的解析ツールです。「静的」解析であり、実際に動作させずに型チェック等を実施できます。

phpstan/phpstan

インストール

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メソッドは末尾の数字以上のレベルを指定した場合、エラーになるようにしています。露骨すぎるとは思いますが、あくまで挙動の確認用ということでご理解ください。

src/Sample.php
<?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を見れば良かった気もします……。