さわって慣れるPHP Parser - NameResolver
さわって慣れるPHP Parser - IdentifierとNameとFullyQualifiedの続編です。
PHP ParserはNameResolverという仕組みを持っています。今回はNameResolverに注目してみます。ドキュメントによると関数、クラス、インターフェース、トレイト、定数の宣言を表すノードにはnamespacedName
プロパティがあり、名前空間付きのクラス名等がNameResolverにより設定されるそうです。
実際に動かして挙動を観察してみたいと思います。
-N
または--resolve-names
オプションによりNameResolver
を利用することができると分かります。
$ vendor/bin/php-parse --help
Usage: php-parse [operations] file1.php [file2.php ...]
or: php-parse [operations] "<?php code"
Turn PHP source code into an abstract syntax tree.
Operations is a list of the following options (--dump by default):
-d, --dump Dump nodes using NodeDumper
-p, --pretty-print Pretty print file using PrettyPrinter\Standard
-j, --json-dump Print json_encode() result
--var-dump var_dump() nodes (for exact structure)
-N, --resolve-names Resolve names using NodeVisitor\NameResolver
-c, --with-column-info Show column-numbers for errors (if available)
-P, --with-positions Show positions in node dumps
-r, --with-recovery Use parsing with error recovery
-h, --help Display this page
ドキュメントにも使い方が書かれていますが、プロダクションコードの生きた事例を見てみたいのでphp-parse
[1]を覗いてみます。処理の冒頭でTraverserを準備しています。NameResolverはVisitor[2]のようです。
$traverser = new PhpParser\NodeTraverser();
$traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver);
文脈を省略しますが、--resolve-names
を指定していると$stmts = $traverser->traverse($stmts);
が実行されると分かります。
foreach ($operations as $operation) {
if ('dump' === $operation) {
fwrite(STDERR, "==> Node dump:\n");
echo $dumper->dump($stmts, $code), "\n";
} elseif ('pretty-print' === $operation) {
fwrite(STDERR, "==> Pretty print:\n");
echo $prettyPrinter->prettyPrintFile($stmts), "\n";
} elseif ('json-dump' === $operation) {
fwrite(STDERR, "==> JSON dump:\n");
echo json_encode($stmts, JSON_PRETTY_PRINT), "\n";
} elseif ('var-dump' === $operation) {
fwrite(STDERR, "==> var_dump():\n");
var_dump($stmts);
} elseif ('resolve-names' === $operation) {
fwrite(STDERR, "==> Resolved names.\n");
$stmts = $traverser->traverse($stmts);
}
}
$stmts
をtraverse()
して$stmts
に入れ直しており、$stmts
を何等か加工してそうです。
では観察してみます。
解析対象のコード(Asia.php
)をしたためます。(PHPとしての適切さは度外視し、ノードを観察するためだけのコードです。)
<?php
namespace Red;
class Stone{}
解析します。
$ vendor/bin/php-parse -N --var-dump Asia.php
====> File Asia.php:
==> Resolved names.
==> var_dump():
array(1) {
[0]=>
object(PhpParser\Node\Stmt\Namespace_)#1202 (3) {
["name"]=>
object(PhpParser\Node\Name)#1201 (2) {
["parts"]=>
array(1) {
[0]=>
string(3) "Red"
}
["attributes":protected]=>
array(4) {
["startLine"]=>
int(2)
["startFilePos"]=>
int(16)
["endLine"]=>
int(2)
["endFilePos"]=>
int(18)
}
}
["stmts"]=>
array(1) {
[0]=>
object(PhpParser\Node\Stmt\Class_)#1204 (8) {
["flags"]=>
int(0)
["extends"]=>
NULL
["implements"]=>
array(0) {
}
["name"]=>
object(PhpParser\Node\Identifier)#1203 (2) {
["name"]=>
string(5) "Stone"
["attributes":protected]=>
array(4) {
["startLine"]=>
int(3)
["startFilePos"]=>
int(27)
["endLine"]=>
int(3)
["endFilePos"]=>
int(31)
}
}
["stmts"]=>
array(0) {
}
["attrGroups"]=>
array(0) {
}
["namespacedName"]=>
object(PhpParser\Node\Name)#1205 (2) {
["parts"]=>
array(2) {
[0]=>
string(3) "Red"
[1]=>
string(5) "Stone"
}
["attributes":protected]=>
array(0) {
}
}
["attributes":protected]=>
array(4) {
["startLine"]=>
int(3)
["startFilePos"]=>
int(21)
["endLine"]=>
int(3)
["endFilePos"]=>
int(33)
}
}
}
["attributes":protected]=>
array(5) {
["startLine"]=>
int(2)
["startFilePos"]=>
int(6)
["endLine"]=>
int(3)
["endFilePos"]=>
int(33)
["kind"]=>
int(1)
}
}
}
色々でてますが読み流してnamespacedName
プロパティを見つけました。解析したのはClass_
ノードですが、これが継承するClassLike
がnamespacedName
プロパティを持っています。ちなみに、Enum_
, Interface_
, Trait_
もClassLike
を継承しています。
["namespacedName"]=>
object(PhpParser\Node\Name)#1205 (2) {
["parts"]=>
array(2) {
[0]=>
string(3) "Red"
[1]=>
string(5) "Stone"
}
クラスの修飾名が格納されていることがわかりました。
もう一つ試してみます。
<?php
use Super\Asia;
new Asia();
解析結果を抜粋します。
[1]=>
object(PhpParser\Node\Stmt\Expression)#1206 (2) {
["expr"]=>
object(PhpParser\Node\Expr\New_)#1205 (3) {
["class"]=>
object(PhpParser\Node\Name\FullyQualified)#1208 (2) {
["parts"]=>
array(2) {
[0]=>
string(5) "Super"
[1]=>
string(4) "Asia"
}
クラスの完全修飾名が格納されていることがわかりました。
ところでPHP Parserのコードベースを$namespacedName
で検索してみます。3つヒットしました。
Const_
ClassLike
Function_
ClassLike
は抽象クラスで、具象クラスは以下の4つです。
Class_
Enum_
Interface_
Trait_
まとめると、定数、関数、クラス、列挙型、インターフェース、トレイトにはNameResolverが解決した(完全)修飾名を格納できることがわかりました。これらは名前空間付きで識別される者たちであると受け取りました。PHPの仕様に若干詳しくなれた気がします。
ただドキュメントにEnumが明記されていなかったのが気になります。観察してみましょう。
<?php
namespace Cars;
enum MenOfPillar
{
case Acdc;
case Wham;
case Santana;
}
解析結果を抜粋します。namespacedName
がありました。
["namespacedName"]=>
object(PhpParser\Node\Name)#1211 (2) {
["parts"]=>
array(2) {
[0]=>
string(4) "Cars"
[1]=>
string(11) "MenOfPillar"
}
["attributes":protected]=>
array(0) {
}
}
ドキュメントによるとNameResolverの引数にオプションを渡せるようです。解析結果を観察したくなりますがひとまず我慢します。解析結果を格納する入れ物には以下4つのパターンがあるということとざっくり理解しました。
- namespacedName property(上述)
- originalName attribute
- resolvedName attribute
- namespacedName attribute
ひとつめはproperty、残る3つはattributeです。attributeというのは言語仕様のアトリビュートではなくノードに付随するPHP Parserが与える情報です。実は上述の解析結果の中にもありました。
さて、上述した通り、NameResolverはVisitorということでした。これはenterNode()[3]を実装しています。雑に読んでみます。
public function enterNode(Node $node) {
if ($node instanceof Stmt\Namespace_) {
$this->nameContext->startNamespace($node->name);
} elseif ($node instanceof Stmt\Use_) {
foreach ($node->uses as $use) {
$this->addAlias($use, $node->type, null);
}
} elseif ($node instanceof Stmt\GroupUse) {
foreach ($node->uses as $use) {
$this->addAlias($use, $node->type, $node->prefix);
}
} elseif ($node instanceof Stmt\Class_) {
if (null !== $node->extends) {
$node->extends = $this->resolveClassName($node->extends);
}
foreach ($node->implements as &$interface) {
$interface = $this->resolveClassName($interface);
}
$this->resolveAttrGroups($node);
if (null !== $node->name) {
$this->addNamespacedName($node);
}
} elseif ($node instanceof Stmt\Interface_) {
foreach ($node->extends as &$interface) {
$interface = $this->resolveClassName($interface);
}
$this->resolveAttrGroups($node);
$this->addNamespacedName($node);
} elseif ($node instanceof Stmt\Enum_) {
foreach ($node->implements as &$interface) {
$interface = $this->resolveClassName($interface);
}
$this->resolveAttrGroups($node);
if (null !== $node->name) {
$this->addNamespacedName($node);
}
} elseif ($node instanceof Stmt\Trait_) {
$this->resolveAttrGroups($node);
$this->addNamespacedName($node);
} elseif ($node instanceof Stmt\Function_) {
$this->resolveSignature($node);
$this->resolveAttrGroups($node);
$this->addNamespacedName($node);
} elseif ($node instanceof Stmt\ClassMethod
|| $node instanceof Expr\Closure
|| $node instanceof Expr\ArrowFunction
) {
$this->resolveSignature($node);
$this->resolveAttrGroups($node);
} elseif ($node instanceof Stmt\Property) {
if (null !== $node->type) {
$node->type = $this->resolveType($node->type);
}
$this->resolveAttrGroups($node);
} elseif ($node instanceof Stmt\Const_) {
foreach ($node->consts as $const) {
$this->addNamespacedName($const);
}
} else if ($node instanceof Stmt\ClassConst) {
$this->resolveAttrGroups($node);
} else if ($node instanceof Stmt\EnumCase) {
$this->resolveAttrGroups($node);
} elseif ($node instanceof Expr\StaticCall
|| $node instanceof Expr\StaticPropertyFetch
|| $node instanceof Expr\ClassConstFetch
|| $node instanceof Expr\New_
|| $node instanceof Expr\Instanceof_
) {
if ($node->class instanceof Name) {
$node->class = $this->resolveClassName($node->class);
}
} elseif ($node instanceof Stmt\Catch_) {
foreach ($node->types as &$type) {
$type = $this->resolveClassName($type);
}
} elseif ($node instanceof Expr\FuncCall) {
if ($node->name instanceof Name) {
$node->name = $this->resolveName($node->name, Stmt\Use_::TYPE_FUNCTION);
}
} elseif ($node instanceof Expr\ConstFetch) {
$node->name = $this->resolveName($node->name, Stmt\Use_::TYPE_CONSTANT);
} elseif ($node instanceof Stmt\TraitUse) {
foreach ($node->traits as &$trait) {
$trait = $this->resolveClassName($trait);
}
foreach ($node->adaptations as $adaptation) {
if (null !== $adaptation->trait) {
$adaptation->trait = $this->resolveClassName($adaptation->trait);
}
if ($adaptation instanceof Stmt\TraitUseAdaptation\Precedence) {
foreach ($adaptation->insteadof as &$insteadof) {
$insteadof = $this->resolveClassName($insteadof);
}
}
}
}
return null;
}
雑に読みます。
- ノードの種類によって処理が分かれている。
- 上3つ(
Namespace_
,Use_
,GroupUse
)は名前空間を特定する部分っぽくて、そこから下のノードはそれを受け取りpropertyやattributeに設定する側っぽい。 -
Class_
,Interface_
,Enum_
,Trait_
,Function_
はちょっとした差異がありつつ雰囲気が似てて、$this->resolveAttrGroups($node)
と$this->addNamespacedName($node)
でごにょごにょしてる。 -
addNamespacedName()
でnamespacedName propertyを設定してる。 -
ClassMethod
,Closure
,ArrowFunction
には$this->addNamespacedName($node)
がない。ちなみに、Function_
,ClassMethod
,Closure
,ArrowFunction
は全てFunctionLike
を継承する仲間。なんだけど名前空間の扱いについてはFunction_
と差異がある。性質の違いがこういうところに表れてる。 - (この時点で
$namespacedName
を有する6つのうちConst_
だけがまだ登場してないなあ) -
Property
には$this->resolveAttrGroups($node)
だけがある。 -
Const_
キタ。$this->resolveAttrGroups($node)
がない。アトリビュートは定数に対して付加されないと。 -
ClassConst
とEnumCase
には$this->resolveAttrGroups($node)
だけがある。 -
StaticCall
以下はざっくりとName
ノードを内包するノードっぽいけど全部ではなさそう。
なるほど。
最後にName
ノードを内包するノードの分析結果を観察してみたいと思います。
ただその前にノードに関するTipsに触れてみます。
関数には宣言と使用(呼び出し)の2つの側面があり、ノードもそれぞれ異なるという風に私は捉えています。前者はFunction_
、後者はFuncCall
といった具合です。FuncCall
には関数名を表す$name
というプロパティがありこれはName
ノード(またはExpr
ノードなんですが発散するので割愛します)です。
クラスも同様に私は捉えています。宣言はClass_
、それ以外は例えばStaticCall
においてクラス名を表す$class
プロパティ(これもName
またはExpr
ノード)や、引数や戻り値の型を表すノードにおいてName
として現れたりします。
このように宣言と、それを使用する部分を混同しないように注意しています。
話を戻します。
NameResolverが解析結果を格納する入れ物が4つあるということでした。その内の1つは、宣言のノードたちが持つnamespacedName propertyでした。一方、Name
ノードは宣言のノードではなくそれを持ちません。ですがNameResolverのpreserveOriginalNames
オプションをtrueにしてやるとoriginalName attributeに値を設定するようになるようです。
ではやってみます。php-parseを細工します。
- $traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver);
+ $traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['preserveOriginalNames' => true]));
StaticCall
のケースを試してみます。
<?php
use Tibet\Dire;
Dire::introduceMyself();
解析結果を抜粋します。
object(PhpParser\Node\Expr\StaticCall)#1206 (4) {
["class"]=>
object(PhpParser\Node\Name\FullyQualified)#1209 (2) {
["parts"]=>
array(2) {
[0]=>
string(5) "Tibet"
[1]=>
string(4) "Dire"
}
["attributes":protected]=>
array(5) {
["startLine"]=>
int(3)
["startFilePos"]=>
int(22)
["endLine"]=>
int(3)
["endFilePos"]=>
int(25)
["originalName"]=>
object(PhpParser\Node\Name)#1204 (2) {
["parts"]=>
array(1) {
[0]=>
string(4) "Dire"
}
Dire
クラスが完全修飾名(FullyQualified
)で解決されており、FullyQualified
のattributes
にoriginalName
がいました。originalName
には名前解決されてないクラス名が格納されていることを確認できました。
-
PHP Parser v4.16.0で確認。 ↩︎
Discussion