🐶

PHPStanのテストコードを書くのに役立つRectorやPHP Parserの知識

2023/10/29に公開

先日PHPStanのテストコードを書く機会があり、発見があったので控えておく。

お題はTypeSpecifierのテスト。TypeSpecifierのテストがtests/PHPStan/Analyser/TypeSpecifierTest.phpにある。一部を抜粋する。(phpstan/phpstan-src)

    /**
     * @dataProvider dataCondition
     * @param mixed[] $expectedPositiveResult
     * @param mixed[] $expectedNegatedResult
     */
    public function testCondition(
        Expr $expr, 
        array $expectedPositiveResult, 
        array $expectedNegatedResult
    ): void {}

    public function dataCondition(): iterable
    {
        if (PHP_VERSION_ID >= 80100) {
            yield [
                new Identical(
                    new PropertyFetch(new Variable('foo'), 'bar'),
                    new Expr\ClassConstFetch(new Name('Bug9499\\FooEnum'), 'A'),
                ),
                [
                    '$foo->bar' => 'Bug9499\FooEnum::A',
                ],
                [
                    '$foo->bar' => '~Bug9499\FooEnum::A',
                ],
            ];

これがどういうテストなのかはさておき、テストデータの1つ目の要素(new Identicalの部分)に注目。

これは解析対象のノードを生成するロジックなんだけど、見慣れてないと「??」ってなる気がする。ぱっとみRectorのNode Overviewを連想させる見た目。

テストを書こうとする時、まずはテストしたいPHPのコードが頭の中にあるはずで、それをこのようなnew ノードの様式にマッピングする必要がある。慣れれば頭の中でできるかもしれないが、PHP Parserを使って解析すれば手っ取り早く確実に変換できる。そのやり方をここにまとめた。(--var-dumpモードで解析した方がわかりやすい。) これで、どんなノードをnewすればよいか、どんなノードがどんなノードを内包すればよいかがわかる。

ただし、PHP Parserの実行結果はnew ノード形式ではないので、まんまコピペすることができない。ゆえに、転記し間違える可能性がある。信頼できないテストコードはむしろ害になるので正しく書きたい。なので、狙った通りのPHPコードが復元されるかどうかを確かめたい。そこでRectorのカスタムルールが利用できる。カスタムルールの書き方はこちら

以下のようなカスタムルールと置換対象のコードを作成して、Rectorを実行するとPHPのコードが得られる。

カスタムルール

    public function getNodeTypes(): array
    {
        // ノードはなんだってよい。便宜的にVariableにした
        return [Variable::class];
    }

    /**
     * @param Variable $node
     */
    public function refactor(Node $node): ?Node
    {
        $variableName = $this->getName($node);
        // 無限再帰しないように変換後の変数名と異なる変数名にする
        if ($variableName !== 'unluckyTakuo') {
            return null;
        }
        // テストデータの1つ目の要素をreturnする
        return new Identical(
            new PropertyFetch(new Variable('foo'), 'bar'),
            new Expr\ClassConstFetch(new Name('Bug9499\\FooEnum'), 'A'),
        );
    }

置換対象のコード

<?php
$unluckyTakuo;

<?php
$foo->bar === Bug9499\FooEnum::A;

置換後のコードがテストしたいPHPのコードであればOK。

以上

Discussion