【静的解析】Larastan(PHPStan)の未使用プライベートクラス定数の誤検知エラーを回避してみよう
こんにちは、モリです。 今回は静的解析ツールであるLarastan(PHPStan)の未使用プライベートクラス定数についての誤検知への対処方法について紹介したいと思います。
環境
- PHP: 8.1
 - Laravel Framework: 9.19.0
 - nunomaduro/larastan: 2.2.9
- 解析レベル8
 
 - myclabs/php-enum: 1.83
 
問題となるコードとエラー
まず実際に出力されるエラーと対象のコードについて見ていきたいと思います。
use MyCLabs\Enum\Enum;
class Gender extends Enum
{
    /** 男性 */
    private const MALE = 1;
    /** 女性 */
    private const FEMALE = 2;
    /** その他 */
    private const OTHER = 3;
}
 ------ ------------------------------------------------------------------------------- 
  Line   Enum/Gender.php                                                             
 ------ ------------------------------------------------------------------------------- 
  18     Constant App\Enum\Gender::MALE is unused.                                   
         💡 See: https://phpstan.org/developing-extensions/always-used-class-constants  
  21     Constant App\Enum\Gender::FEMALE is unused.                                 
         💡 See: https://phpstan.org/developing-extensions/always-used-class-constants  
  24     Constant App\Enum\Gender::OTHER is unused.                                  
         💡 See: https://phpstan.org/developing-extensions/always-used-class-constants  
 ------ ------------------------------------------------------------------------------- 
上記はmyclabs/php-enumライブラリを利用したEnumクラスになります。
ちなみにmyclabs/php-enumについてですが、PHP8.1 でEnumが実装されるまで、独自のEnumライクなクラスを定義できるライブラリとしてお世話になっていました。
こちらをPHPStanが解析レベル4以上で静的解析すると未使用のプライベートクラス定数を検出し、エラーとなります。 しかし、静的解析では理解できないプライベート定数の読み書きがコード上で実際には行われており、正しい実装です。
では、このエラーを回避するにはどうすればよいでしょうか? 該当箇所に@phpstan-ignore-next-lineを記述したり、PHPStan設定ファイル(phpstan.neon.dist)のignoreErrorsやexcludePathsに無視したいエラーやファイル名を記述することもできますが、どれも本末転倒なやり方に過ぎません。
上記エラーを無視せずに解決する方法を紹介します。
その答えはエラーメッセージにある
もう一度エラーメッセージの一部を見てみます。
 ------ ------------------------------------------------------------------------------- 
  18     Constant App\Enum\Gender::MALE is unused.                                   
         💡 See: https://phpstan.org/developing-extensions/always-used-class-constants  
 ------ ------------------------------------------------------------------------------- 
phpstan公式ドキュメントのリンクが記載されています。 リンク先に書かれている対処方法を次より実践していきます。
公式interfaceを実装したクラスの作成
ドキュメントにも記載がありますが、下記のinterfaceが用意されています。
namespace PHPStan\Rules\Constants;
use PHPStan\Reflection\ConstantReflection;
interface AlwaysUsedClassConstantsExtension
{
    public function isAlwaysUsed(ConstantReflection $constant): bool;
}
上記interfaceを実装したConstantsExtensionを作成します。
namespace App\Enum;
use PHPStan\Reflection\ConstantReflection;
use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtension;
class ConstantsExtension implements AlwaysUsedClassConstantsExtension
{
    public function isAlwaysUsed(ConstantReflection $constant): bool
    {
        return true;
    }
}
上記のようにtrueを返すことで、「クラス定数は実際には使用されている」とPHPStanに理解させて、エラー検知を回避するクラスを作成したことになります。 が、これではあらゆるクラスの定数未使用エラーを無視してしまうことと変わりありません。したがって今回に限っては下記のように、myclabs/php-enumライブラリのEnumクラスを継承したクラスに限定しておきます。
namespace App\Enum;
use MyCLabs\Enum\Enum;
use PHPStan\Reflection\ConstantReflection;
use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtension;
class ConstantsExtension implements AlwaysUsedClassConstantsExtension
{
    public function isAlwaysUsed(ConstantReflection $constant): bool
    {
        if ($constant->getDeclaringClass()->isSubclassOf(Enum::class)) {
            return true;
        }
        return false;
    }
}
作成したクラスをPHP設定ファイルに登録
ConstantsExtensionクラスを作成したら、PHPStan設定ファイル(phpstan.neon.dist)に登録する必要があります。 登録する前がこちら。
includes:
  - ./vendor/nunomaduro/larastan/extension.neon
parameters:
  paths:
    - app
  # The level 8 is the highest level
  level: 8
  checkGenericClassInNonGenericObjectType: false
  checkMissingIterableValueType: false
  checkUnionTypes: false
  reportMaybes: false
これにservicesを追加して登録します。 classには作成したクラスを、tagsにはエラー回避用Extensionを記述しておきます。
includes:
  - ./vendor/nunomaduro/larastan/extension.neon
parameters:
  paths:
    - app
  # The level 8 is the highest level
  level: 8
  checkGenericClassInNonGenericObjectType: false
  checkMissingIterableValueType: false
  checkUnionTypes: false
  reportMaybes: false
services:
  -
    class: App\Enum\ConstantsExtension
    tags:
      - phpstan.constants.alwaysUsedClassConstantsExtension
これで完了です。 再度、静的解析を実行してみます。
[OK] No errors
誤検知されていたエラーを解消することができました。
最後に
今回は、Larastan(PHPStan)の誤検知エラーを回避する方法について一例をご紹介しました。 安直にエラーをignoreせずに、公式に用意されている回避方法を試して実装してみてください。
以上となります。ありがとうございました。