Skip to content

Add NodeIterator #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
92 changes: 92 additions & 0 deletions docs/Traversal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@

# AST Traversal

All Nodes implement the `IteratorAggregate` interface, which means their immediate children can be directly traversed with `foreach`:

```php
foreach ($node as $key => $child) {
var_dump($key)
var_dump($child);
}
```

`$key` is set to the child name (e.g. `parameters`).
Multiple child nodes may have the same key.

The Iterator that is returned to `foreach` from `$node->getIterator()` implements the `RecursiveIterator` interface.
To traverse all descendant nodes, you need to "flatten" it with PHP's built-in `RecursiveIteratorIterator`:

```php
$it = new \RecursiveIteratorIterator($node, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($it as $node) {
var_dump($node);
}
```

The code above will walk all nodes and tokens depth-first.
Passing `RecursiveIteratorIterator::CHILD_FIRST` would traverse breadth-first, while `RecursiveIteratorIterator::LEAVES_ONLY` (the default) would only traverse terminal Tokens.

## Exclude Tokens

To exclude terminal Tokens and only traverse Nodes, use PHP's built-in `ParentIterator`:

```php
$nodes = new \ParentIterator(new \RecursiveIteratorIterator($node, \RecursiveIteratorIterator::SELF_FIRST));
```

## Skipping child traversal

To skip traversal of certain Nodes, use PHP's `RecursiveCallbackIterator`.
Naive example of traversing all nodes in the current scope:

```php
// Find all nodes in the current scope
$nodesInScopeReIt = new \RecursiveCallbackFilterIterator($node, function ($current, string $key, \RecursiveIterator $it) {
// Don't traverse into function nodes, they form a different scope
return !($current instanceof Node\Expression\FunctionDeclaration);
});
// Convert the RecursiveIterator to a flat Iterator
$it = new \RecursiveIteratorIterator($nodesInScope, \RecursiveIteratorIterator::SELF_FIRST);
```

## Filtering

Building on that example, to get all variables in that scope us a non-recursive `CallbackFilterIterator`:

```php
// Filter out all variables
$vars = new \CallbackFilterIterator($it, function ($current, string $key, \Iterator $it) {
return $current instanceof Node\Expression\Variable && $current->name instanceof Token;
});

foreach ($vars as $var) {
echo $var->name . PHP_EOL;
}
```

## Traversing ancestors

Use the `NodeAncestorIterator` to walk the AST upwards from a Node to the root.
Example that finds the closest namespace Node to a Node:

```php
use Microsoft\PhpParser\Iterator\NodeAncestorIterator;
use Microsoft\PhpParser\Node;

foreach (new NodeAncestorIterator($node) as $ancestor) {
if ($ancestor instanceof Node\Statement\NamespaceDefinition) {
var_dump($ancestor->name);
break;
}
}
```

## Converting to an array

You can convert your iterator to a flat array with

```php
$arr = iterator_to_array($it, true);
```

The `true` ensures that the array is indexed numerically and not by Iterator keys (otherwise later Nodes with the same key will override previous Nodes).
Empty file removed docs/a.md
Empty file.
2 changes: 2 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
<file>tests/api/getResolvedName.php</file>
<file>tests/api/PositionUtilitiesTest.php</file>
<file>tests/api/TextEditTest.php</file>
<file>tests/api/NodeIteratorTest.php</file>
<file>tests/api/NodeAncestorIteratorTest.php</file>
</testsuite>

<testsuite name="performance">
Expand Down
75 changes: 75 additions & 0 deletions src/Iterator/NodeAncestorIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
declare(strict_types = 1);

namespace Microsoft\PhpParser\Iterator;

use Microsoft\PhpParser\Node;

/**
* An Iterator to walk the ancestors of a Node up to the root
*/
class NodeAncestorIterator implements \Iterator {

/**
* @var Node
*/
private $start;

/**
* @var Node
*/
private $current;

/**
* @param Node $node The node to start with
*/
public function __construct(Node $node) {
$this->start = $node;
}

/**
* Rewinds the Iterator to the beginning
*
* @return void
*/
public function rewind() {
$this->current = $this->start;
}

/**
* Returns `true` if `current()` can be called to get the current node.
* Returns `false` if the last Node was the root node.
*
* @return bool
*/
public function valid() {
return $this->current !== null;
}

/**
* Always returns null.
*
* @return null
*/
public function key() {
return null;
}

/**
* Returns the current Node
*
* @return Node
*/
public function current() {
return $this->current;
}

/**
* Advances the Iterator to the parent of the current Node
*
* @return void
*/
public function next() {
$this->current = $this->current->parent;
}
}
144 changes: 144 additions & 0 deletions src/Iterator/NodeIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php
declare(strict_types = 1);

namespace Microsoft\PhpParser\Iterator;

use Microsoft\PhpParser\{Node, Token};

/**
* An Iterator to the descendants of a Node
*/
class NodeIterator implements \RecursiveIterator {

/**
* The Node being iterated
*
* @var Node
*/
private $node;

/**
* Iterator used to iterate the child names of a Node
*
* @var Iterator
*/
private $childNamesIterator;

/**
* Iterator used to iterate the child nodes at the current child name
*
* @var Iterator
*/
private $valueIterator;

/**
* @param Node $node The node that should be iterated
*/
public function __construct(Node $node) {
$this->node = $node;
$this->childNamesIterator = new \ArrayIterator($node::CHILD_NAMES);
$this->valueIterator = new \EmptyIterator();
}

/**
* Rewinds the Iterator to the beginning
*
* @return void
*/
public function rewind() {
// Start child names from beginning
$this->childNamesIterator->rewind();
// Begin new children until found a valid one
while ($this->childNamesIterator->valid()) {
$this->beginChild();
if ($this->valueIterator->valid()) {
break;
}
$this->childNamesIterator->next();
}
}

/**
* Returns `true` if `current()` can be called to get the current child.
* Returns `false` if this Node has no more children (direct descendants).
*
* @return bool
*/
public function valid() {
return $this->childNamesIterator->valid() && $this->valueIterator->valid();
}

/**
* Returns the current child name being iterated.
* Multiple values may have the same key.
*
* @return string
*/
public function key() {
return $this->childNamesIterator->current();
}

/**
* Returns the current child (direct descendant)
*
* @return Node|Token
*/
public function current() {
return $this->valueIterator->current();
}

/**
* Advances the Iterator to the next child (direct descendant)
*
* @return void
*/
public function next() {
// Go to next value of current child name
$this->valueIterator->next();
// Begin new children until found a valid one
while (!$this->valueIterator->valid() && $this->childNamesIterator->valid()) {
$this->childNamesIterator->next();
if (!$this->childNamesIterator->valid()) {
return;
}
$this->beginChild();
}
}

/**
* Initializes the Iterator for iterating the values of the current child name
*
* @return void
*/
private function beginChild() {
$value = $this->node->{$this->childNamesIterator->current()};
// Skip null values
if ($value === null) {
$this->valueIterator = new \EmptyIterator();
return;
}
if (!is_array($value)) {
$value = [$value];
}
$this->valueIterator = new \ArrayIterator($value);
}

/**
* Returns true if the current child is another Node (not a Token)
* and can be used to create another NodeIterator
*
* @return bool
*/
public function hasChildren(): bool {
return $this->valueIterator->current() instanceof Node;
}

/**
* Returns a NodeIterator for the children of the current child Node
*
* @return NodeIterator
*/
public function getChildren() {
return new NodeIterator($this->valueIterator->current());
}
}
11 changes: 10 additions & 1 deletion src/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use Microsoft\PhpParser\Node\Statement\NamespaceDefinition;
use Microsoft\PhpParser\Node\Statement\NamespaceUseDeclaration;

abstract class Node implements \JsonSerializable {
abstract class Node implements \JsonSerializable, \IteratorAggregate {
/** @var array[] Map from node class to array of child keys */
private static $childNames = [];

Expand Down Expand Up @@ -149,6 +149,15 @@ public function getRoot() : Node {
return $node;
}

/**
* Gets an Iterator to iterate all descendant nodes
*
* @return NodeIterator
*/
public function getIterator() {
return new NodeIterator($this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be Iterator\NodeIterator?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woops, yes. didn't change it when I moved it to the namespace.

}

/**
* Gets generator containing all descendant Nodes and Tokens.
*
Expand Down
Loading