🐶

さわって慣れるPHP Parser - Nameノードを内包しているノード探し

2023/07/13に公開

さわって慣れるPHP Parser - Nameノードを内包しているノード探し(訂正版)を書きました。以下は訂正前の内容です。


さわって慣れるPHP Parser - NameResolverの続編です。

Nameノードを内包しているノードにはどのようなものがあるのか、気になったので調べてみました。
宣言された関数、クラス、インターフェース、トレイト、定数を使用する側にはこれだけのバリエーションがあるのですね。

namespace ノード[1] コンストラクタの引数[2]
PhpParser\Node\Expr ClassConstFetch $class
PhpParser\Node\Expr ConstFetch $name
PhpParser\Node\Expr FuncCall $name
PhpParser\Node\Expr Instanceof_ $class
PhpParser\Node\Expr New_ $class
PhpParser\Node\Expr StaticCall $class
PhpParser\Node\Expr StaticPropertyFetch $class
PhpParser\Node\Stmt\TraitUseAdaptation Alias $trait
PhpParser\Node\Stmt\TraitUseAdaptation Precedence $insteadof
PhpParser\Node\Stmt\TraitUseAdaptation Precedence $trait
PhpParser\Node\Stmt Catch_ $types
PhpParser\Node\Stmt GroupUse $prefix
PhpParser\Node\Stmt Namespace_ $name
PhpParser\Node\Stmt Property $type
PhpParser\Node\Stmt TraitUse $traits
PhpParser\Node\Stmt UseUse $name
PhpParser\Node Attribute $name
PhpParser\Node IntersectionType $types
PhpParser\Node Name $name
PhpParser\Node NullableType $type
PhpParser\Node Param $type
PhpParser\Node UnionType $types

抽出方法

考え方

  • Nameノードを内包しているノードを探したい。
    Name型のプロパティを持つノードのクラスを探したい。
  • 原則、コンストラクタでプロパティに初期値を設定するはず。
    → コンストラクタの引数から型を評価できるはず。
  • ノードを表すクラスはvendor/nikic/php-parser/lib/PhpParser/Nodeにある。

前提

  • Rector 0.15.23
  • PHPStan 1.10.10
  • PHP Parser 4.15.4

実装

これはRectorの本来の使い方ではありませんが、車輪の再発明なしに型やノードがとれるので、簡易ツールの実装に利用させてもらいました。

NameNodeHolderFinder.php

<?php declare(strict_types=1);

namespace Utils\Rector\Rector;

use PhpParser\Node;
use PhpParser\Node\Expr\Error;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Namespace_;
use PHPStan\Type\ArrayType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\MethodName;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class NameNodeHolderFinder extends AbstractRector
{
    /**
     * @return array<class-string<Node>>
     */
    public function getNodeTypes(): array
    {
        return [Class_::class];
    }

    /**
     * @param Class_ $node
     */
    public function refactor(Node $node): ?Node
    {
        $constructor = $node->getMethod(MethodName::CONSTRUCT);
        if ($constructor === null) {
            return null;
        }
        foreach ($constructor->params as $param) {
            $nameNodeHolder = $this->getNodeHolderIfHeldNodeTypeIsName(
                $node,
                $param
            );
            if ($nameNodeHolder !== '') {
                echo $nameNodeHolder . PHP_EOL;
            }
        }
        return null;
    }

    private function getNodeHolderIfHeldNodeTypeIsName(Class_ $class, Param $param): string
    {
        $type = $this->arrayToNonArray($this->getType($param));
        if ($type instanceof UnionType) {
            foreach ($type->getTypes() as $innerType) {
                $nameNodeHolder = $this->stringifyNameNodeHolder($class, $param, $innerType);
                if ($nameNodeHolder !== '') {
                    return $nameNodeHolder;
                }
            }
        }
        return $this->stringifyNameNodeHolder($class, $param, $type);
    }

    private function stringifyNameNodeHolder(Class_ $class, Param $param, Type $type): string
    {
        if (!$this->arrayToNonArray($type)->equals(new ObjectType('PhpParser\Node\Name'))) {
            return '';
        }
        $namespace = $class->getAttribute(AttributeKey::PARENT_NODE);
        if (!$namespace instanceof Namespace_) {
            return '';
        }
        if ($param->var instanceof Error) {
            return '';
        }
        return $namespace->name?->toString() . ',' . $class->name?->toString() . ',' . '$' . $this->getName($param->var);
    }

    private function arrayToNonArray(Type $type): Type
    {
        return ($type instanceof ArrayType) ? $type->getItemType() : $type;
    }

    public function getRuleDefinition(): RuleDefinition
    {
        return new RuleDefinition('', []);
    }
}

rector.php

<?php declare(strict_types=1);

use Utils\Rector\Rector\NameNodeHolderFinder;
use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([
        __DIR__ . '/vendor/nikic/php-parser/lib/PhpParser/Node',
    ]);

    $rectorConfig->rules([
        NameNodeHolderFinder::class,
    ]);
};

実装備忘録

  • コンストラクタではなくプロパティを分析対象にすると型がとれない。
  • 型指定のバリエーションは、NameName[]、それぞれについて単体で宣言する場合とユニオンの場合がある。
脚注
  1. ノードの種類に関する参考資料: PHP Parserで学ぶPHP ↩︎

  2. プロパティとほぼ一致するがNameノードに関しては、$name$partsプロパティに詰め替える。 ↩︎

Discussion