PHPStanのカスタムルールを作ってみる

# 初めに
PHPの静的解析ツールでおなじみのPHPStanではユーザ独自のカスタムルールを作成することができます。単純なルールであればサクッと作ることができておすすめなので、今回はPHPStanの簡単なカスタムルールを作ってみようと思います。

:::note info
説明で使用するPHPStanのバージョンは`1.12`系です。
:::


# （前提知識）PHPStanのルール

PHPStanで定義されている全てのルールは以下の[Rule](https://github.com/phpstan/phpstan-src/blob/2.0.x/src/Rules/Rule.php)というInterfaceを継承して作る必要があります。

```php:PHPStanが提供しているRuleインターフェース
<?php declare(strict_types = 1);

namespace PHPStan\Rules;

use PhpParser\Node;
use PHPStan\Analyser\Scope;

interface Rule
{

	/**
	 * @phpstan-return class-string<TNodeType>
	 */
	public function getNodeType(): string;

	/**
	 * @phpstan-param TNodeType $node
	 * @return (string|RuleError)[] errors
	 */
	public function processNode(Node $node, Scope $scope): array;

}
```

インターフェースには`getNodeType()`と`processNode()`の二つのメソッドがあり、それぞれ**解析対象**と**解析ルール**の役割を担います。

| メソッド | 役割 |
|:-:|:-:|
| `getNodeType()` | 解析する対象を定める |
| `processNode()` | 解析するルールを定める |

### `getNodeType()`
`getNodeType()`には解析対象を定義します。ここで返すのはPHP-ParserのNodeを継承したクラスの名前です。PHPStanではNodeを継承したクラスが細かく用意されており、様々な場所を解析対象に定めることができます（[このディレクトリにあります](https://github.com/phpstan/phpstan-src/tree/1.12.x/src/Node)）。


### `processNode()`
`processNode()`には解析するルールを定義します。返り値のarrayにはエラー時に出力する文章を定義します。エラーがない場合は空配列で問題ありません。
引数の`$ode`と`$scope`には解析対象の情報が詰められており、これを元に解析でエラーを出すかどうかを決めることができます。


# 実際に作ってみる
というわけで作ってみましょう。
今回は練習として、「**`Hoge`クラスのメソッドは`get`か`post`で始まらないといけない**」というルールを実装してみました（いかにもプロジェクト固有なルールで恐縮ですが、、、）。

## Ruleを作る
まずは以下のようなRuleを実装してみました。

```php: MethodNameRule.php
<?php

declare(strict_types=1);

namespace Test\rule;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;

class MethodNameRule implements Rule
{
    public function getNodeType(): string
    {
        return Node\Stmt\ClassMethod::class;
    }

    /**
     * @param Node\Stmt\ClassMethod $node
     * @param Scope $scope
     * @return string[]
     */
    public function processNode(Node $node, Scope $scope): array
    {
        $className = $scope->getClassReflection()?->getName();

        // 対象クラスを A のみに制限
        // NOTE: $classNameはnamespaceもついてくるのでstr_ends_withで比較
        if (!str_ends_with($className, '\\Hoge')) {
            return [];
        }

        $methodName = $node->name->toString();

        // 'get' または 'post' で始まっているかをチェック
        if (!preg_match('/^(get|post)/', $methodName)) {
            return [sprintf(
                'メソッドの名前はgetかpostで始めてください！ %s::%s',
                $className,
                $methodName
            )];
        }

        return [];
    }
}

```

`getNodeType()`では`Node\Stmt\ClassMethod`のクラス名をreturnすることで、解析対象をクラスのメソッドだけに絞っています。
また、`processNode()`では`$scope`が持っているクラス名の情報を利用し`Hoge`メソッドのみが対象となるように指定します。その後、`$node`が持っているメソッド名を正規表現にかけ解析エラーかどうかを判断します。

## 設定ファイル（.neon）ファイルに記載
作成したルールはphpstan.neonファイル以下のように記載すればOKです。

```yaml: phpstan.neon
rules:
    - Test\rule\MethodNameRule
```


ちなみに、ルール自体にコンストラクタなどを定義している場合、rulesではなくServiceなどに定義が必要です。詳しくは[公式ドキュメント](https://phpstan.org/developing-extensions/rules)をご覧ください。

## 実行してみる
それでは実行してみます。
テスト用に以下のクラスを作成してみました。
```php
<?php
namespace Test\analyseObject;

class Hoge
{
    public function getTest(): void {}

    public function postTest(): void {}

    public function putTest(): void {}
}
```

解析をかけた結果がこちらです。ちゃんと意図したところでだけエラーがでるのが確認できました！
```
./vendor/bin/phpstan analyse
Note: Using configuration file C:\path\to\project\phpstan.neon.
 9/9 [============================] 100%
 
 ------ ------------------------------------------------------------------------------------------ 
  Line   analyseObject\Hoge.php
 ------ ------------------------------------------------------------------------------------------
  :10    メソッドの名前はgetかpostで始めてください！ Test\analyseObject\Hoge::putTest
         ✏️  analyseObject\Hoge.php
 ------ ------------------------------------------------------------------------------------------
 ```


# 終わりに
今回はPHPStanのカスタムルールを作成してみました。
chatGPTにいろいろ聞きながら作ってみましたが、サクッと作れていい感じです。
プロジェクト固有のルールはどこにでもあると思うので、レビューでよく指摘する内容などはルールに起こすのもアリだと思います。

ここまで読んでいただきありがとうございました！

# 参考
https://zenn.dev/pixiv/articles/1e6cade978808a