1
0
mirror of https://github.com/danog/PHP-Parser.git synced 2024-11-29 20:09:08 +01:00

Add ParentConnectingVisitor and NodeConnectingVisitor (#681)

This commit is contained in:
Sebastian Bergmann 2020-07-11 11:31:43 +02:00 committed by GitHub
parent c346bbfafe
commit f545f18a87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 188 additions and 45 deletions

View File

@ -7,28 +7,21 @@ Frequently Asked Questions
How can the parent of a node be obtained?
-----
The AST does not store parent nodes by default. However, it is easy to add a custom parent node
attribute using a custom node visitor:
The AST does not store parent nodes by default. However, the `ParentConnectingVisitor` can be used to achieve this:
```php
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\ParentConnectingVisitor;
use PhpParser\ParserFactory;
class ParentConnector extends NodeVisitorAbstract {
private $stack;
public function beforeTraverse(array $nodes) {
$this->stack = [];
}
public function enterNode(Node $node) {
if (!empty($this->stack)) {
$node->setAttribute('parent', $this->stack[count($this->stack)-1]);
}
$this->stack[] = $node;
}
public function leaveNode(Node $node) {
array_pop($this->stack);
}
}
$code = '...';
$traverser = new NodeTraverser;
$traverser->addVisitor(new ParentConnectingVisitor);
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
$ast = $traverser->traverse($ast);
```
After running this visitor, the parent node can be obtained through `$node->getAttribute('parent')`.
@ -36,33 +29,27 @@ After running this visitor, the parent node can be obtained through `$node->getA
How can the next/previous sibling of a node be obtained?
-----
Again, siblings are not stored by default, but the visitor from the previous entry can be easily
extended to store the previous / next node with a common parent as well:
Again, siblings are not stored by default, but the `NodeConnectingVisitor` can be used to store
the previous / next node with a common parent as well:
```php
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NodeConnectingVisitor;
use PhpParser\ParserFactory;
class NodeConnector extends NodeVisitorAbstract {
private $stack;
private $prev;
public function beforeTraverse(array $nodes) {
$this->stack = [];
$this->prev = null;
}
public function enterNode(Node $node) {
if (!empty($this->stack)) {
$node->setAttribute('parent', $this->stack[count($this->stack)-1]);
}
if ($this->prev && $this->prev->getAttribute('parent') == $node->getAttribute('parent')) {
$node->setAttribute('prev', $this->prev);
$this->prev->setAttribute('next', $node);
}
$this->stack[] = $node;
}
public function leaveNode(Node $node) {
$this->prev = $node;
array_pop($this->stack);
}
}
$code = '...';
$traverser = new NodeTraverser;
$traverser->addVisitor(new NodeConnectingVisitor);
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
$ast = $traverser->traverse($ast);
```
After running this visitor, the parent node can be obtained through `$node->getAttribute('parent')`,
the previous node can be obtained through `$node->getAttribute('previous')`, and the next node can be
obtained through `$node->getAttribute('next')`.
`ParentConnectingVisitor` and `NodeConnectingVisitor` should not be used at the same time. The latter
includes the functionality of the former.

View File

@ -0,0 +1,52 @@
<?php declare(strict_types=1);
namespace PhpParser\NodeVisitor;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
/**
* Visitor that connects a child node to its parent node
* as well as its sibling nodes.
*
* On the child node, the parent node can be accessed through
* <code>$node->getAttribute('parent')</code>, the previous
* node can be accessed through <code>$node->getAttribute('previous')</code>,
* and the next node can be accessed through <code>$node->getAttribute('next')</code>.
*/
final class NodeConnectingVisitor extends NodeVisitorAbstract
{
/**
* @var Node[]
*/
private $stack = [];
/**
* @var ?Node
*/
private $previous;
public function beforeTraverse(array $nodes) {
$this->stack = [];
$this->previous = null;
}
public function enterNode(Node $node) {
if (!empty($this->stack)) {
$node->setAttribute('parent', $this->stack[count($this->stack) - 1]);
}
if ($this->previous !== null && $this->previous->getAttribute('parent') === $node->getAttribute('parent')) {
$node->setAttribute('previous', $this->previous);
$this->previous->setAttribute('next', $node);
}
$this->stack[] = $node;
}
public function leaveNode(Node $node) {
$this->previous = $node;
array_pop($this->stack);
}
}

View File

@ -0,0 +1,41 @@
<?php declare(strict_types=1);
namespace PhpParser\NodeVisitor;
use function array_pop;
use function count;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
/**
* Visitor that connects a child node to its parent node.
*
* On the child node, the parent node can be accessed through
* <code>$node->getAttribute('parent')</code>.
*/
final class ParentConnectingVisitor extends NodeVisitorAbstract
{
/**
* @var Node[]
*/
private $stack = [];
public function beforeTraverse(array $nodes)
{
$this->stack = [];
}
public function enterNode(Node $node)
{
if (!empty($this->stack)) {
$node->setAttribute('parent', $this->stack[count($this->stack) - 1]);
}
$this->stack[] = $node;
}
public function leaveNode(Node $node)
{
array_pop($this->stack);
}
}

View File

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace PhpParser\NodeVisitor;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Stmt\Else_;
use PhpParser\Node\Stmt\If_;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\ParserFactory;
final class NodeConnectingVisitorTest extends \PHPUnit\Framework\TestCase
{
public function testConnectsNodeToItsParentNodeAndItsSiblingNodes()
{
$ast = (new ParserFactory)->create(ParserFactory::PREFER_PHP7)->parse(
'<?php if (true) {} else {}'
);
$traverser = new NodeTraverser;
$traverser->addVisitor(new NodeConnectingVisitor);
$ast = $traverser->traverse($ast);
$node = (new NodeFinder)->findFirstInstanceof($ast, Else_::class);
$this->assertSame(If_::class, get_class($node->getAttribute('parent')));
$this->assertSame(ConstFetch::class, get_class($node->getAttribute('previous')));
$node = (new NodeFinder)->findFirstInstanceof($ast, ConstFetch::class);
$this->assertSame(Else_::class, get_class($node->getAttribute('next')));
}
}

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace PhpParser\NodeVisitor;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\ParserFactory;
final class ParentConnectingVisitorTest extends \PHPUnit\Framework\TestCase
{
public function testConnectsChildNodeToParentNode()
{
$ast = (new ParserFactory)->create(ParserFactory::PREFER_PHP7)->parse(
'<?php class C { public function m() {} }'
);
$traverser = new NodeTraverser;
$traverser->addVisitor(new ParentConnectingVisitor);
$ast = $traverser->traverse($ast);
$node = (new NodeFinder)->findFirstInstanceof($ast, ClassMethod::class);
$this->assertSame('C', $node->getAttribute('parent')->name->toString());
}
}