Skip to content
75 changes: 75 additions & 0 deletions docs/Traversal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

# 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, $key, Iterator $iterator) {
// Don't traverse into function nodes, they form a differnt scope
Copy link
Contributor

Choose a reason for hiding this comment

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

typo

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, $key, $iterator) {
return $current instanceof Node\Expression\Variable && $current->name instanceof Token;
});

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

## 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 Nodes later Nodes with the same key will override previous).
Empty file removed docs/a.md
Empty file.
1 change: 1 addition & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<file>tests/api/getResolvedName.php</file>
<file>tests/api/PositionUtilitiesTest.php</file>
<file>tests/api/TextEditTest.php</file>
<file>tests/api/NodeIteratorTest.php</file>
</testsuite>

<testsuite name="performance">
Expand Down
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
133 changes: 133 additions & 0 deletions src/NodeIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php
declare(strict_types = 1);

namespace Microsoft\PhpParser;

/**
* An Iterator to walk a Node and its descendants
*/
class NodeIterator implements \RecursiveIterator {

/**
* 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|null
*/
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();
// If there is a child name, start an iterator for its values
if ($this->childNamesIterator->valid()) {
$this->beginChild();
} else {
$this->valueIterator = new \EmptyIterator();
}
}

/**
* Returns `true` if `current()` can be called to get the current child.
* Returns `false` if this Node has no more children (direct descendants).
*/
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();
while (!$this->valueIterator->valid()) {
// Finished with all values under the current child name
// Go to next child name
$this->childNamesIterator->next();
// If there still is a child name, iterate its value
// Else become invalid
if ($this->childNamesIterator->valid()) {
$this->beginChild();
} else {
break;
}
}
}

/**
* 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) {
$value = [];
} else 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 Node
*
* @return NodeIterator
*/
public function getChildren() {
return new NodeIterator($this->valueIterator->current());
}
}
83 changes: 83 additions & 0 deletions tests/api/NodeIteratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

use PHPUnit\Framework\TestCase;
use Microsoft\PhpParser\{Parser, NodeIterator, Node};

class NodeIteratorTest extends TestCase {

const FILE_CONTENTS = <<<'PHP'
<?php
function foo($x) {
if ($x) {
var_dump($x);
}
}
foo();
PHP;

/** @var Node\SourceFileNode */
private $sourceFile;

public function setUp() {
$parser = new Parser();
$this->sourceFile = $parser->parseSourceFile(self::FILE_CONTENTS);
}

public function testIteratesChildren() {
$iterator = new NodeIterator($this->sourceFile);
$this->assertEquals(
[
$this->sourceFile->statementList[0],
$this->sourceFile->statementList[1],
$this->sourceFile->statementList[2],
$this->sourceFile->endOfFileToken
],
iterator_to_array($iterator, false)
);
}

public function testIteratesDescendants() {
$iterator = new \RecursiveIteratorIterator(new NodeIterator($this->sourceFile), \RecursiveIteratorIterator::SELF_FIRST);
$arr = iterator_to_array($iterator, false);
$this->assertEquals(
[
$this->sourceFile->statementList[0],
$this->sourceFile->statementList[1],
$this->sourceFile->statementList[1]->functionKeyword,
$this->sourceFile->statementList[1]->name,
$this->sourceFile->statementList[1]->openParen,
$this->sourceFile->statementList[1]->parameters,
$this->sourceFile->statementList[1]->parameters->children[0],
$this->sourceFile->statementList[1]->closeParen,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->openBrace,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0],
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->ifKeyword,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->openParen,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->expression,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->closeParen,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->openBrace,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0],
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0]->expression,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0]->expression->callableExpression,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0]->expression->callableExpression->nameParts[0],
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0]->expression->openParen,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0]->expression->argumentExpressionList,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0]->expression->argumentExpressionList->children[0],
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0]->expression->argumentExpressionList->children[0]->expression,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0]->expression->argumentExpressionList->children[0]->expression->name,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0]->expression->closeParen,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->statements[0]->semicolon,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->statements[0]->statements->closeBrace,
$this->sourceFile->statementList[1]->compoundStatementOrSemicolon->closeBrace,
$this->sourceFile->statementList[2]->expression->callableExpression->nameParts[0],
$this->sourceFile->statementList[2]->expression->openParen,
$this->sourceFile->statementList[2]->expression->closeParen,
$this->sourceFile->statementList[2]->semicolon,
$this->sourceFile->endOfFileToken
],
$arr
);
}
}