-
Notifications
You must be signed in to change notification settings - Fork 85
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
Add NodeIterator #139
Changes from 2 commits
99fd539
79e87c8
dca82af
0f82f05
ba494d4
aa0a707
fd6358f
1cb1356
d63e978
d42d70b
6379ba9
23b9868
348abb9
4ee5b75
035e87e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = []; | ||
|
|
||
|
|
@@ -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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be Iterator\NodeIterator?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
| * | ||
|
|
||
| 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(); | ||
| // 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). | ||
| */ | ||
| 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 Node | ||
| * | ||
| * @return NodeIterator | ||
| */ | ||
| public function getChildren() { | ||
| return new NodeIterator($this->valueIterator->current()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| <?php | ||
|
|
||
| use PHPUnit\Framework\TestCase; | ||
| use Microsoft\PhpParser\{Parser, NodeIterator, Node}; | ||
|
|
||
| class NodeIteratorTest extends TestCase { | ||
|
|
||
| const FILE_CONTENTS = ' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the advantage? |
||
| <?php | ||
| function a() { | ||
| $a = 1; | ||
| $b = 2; | ||
| } | ||
| a(); | ||
| '; | ||
|
|
||
| /** @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); | ||
| $iterator->rewind(); | ||
|
|
||
| $this->assertTrue($iterator->valid()); | ||
| $this->assertSame($this->sourceFile->statementList[0], $iterator->current()); | ||
| $iterator->next(); | ||
|
|
||
| $this->assertTrue($iterator->valid()); | ||
| $this->assertSame($this->sourceFile->statementList[1], $iterator->current()); | ||
| $iterator->next(); | ||
|
|
||
| $this->assertTrue($iterator->valid()); | ||
| $this->assertSame($this->sourceFile->statementList[2], $iterator->current()); | ||
| $iterator->next(); | ||
|
|
||
| $this->assertTrue($iterator->valid()); | ||
| $this->assertSame($this->sourceFile->endOfFileToken, $iterator->current()); | ||
| $iterator->next(); | ||
|
|
||
| $this->assertFalse($iterator->valid()); | ||
| } | ||
|
|
||
| public function testRecursiveIteratorIteratorIteratesDescendants() { | ||
|
|
||
| $it = new \RecursiveIteratorIterator(new NodeIterator($this->sourceFile), \RecursiveIteratorIterator::SELF_FIRST); | ||
| $it->rewind(); | ||
|
|
||
| // Node\Statement\InlineHtml | ||
| $this->assertTrue($it->valid()); | ||
| $this->assertSame('statementList', $it->key()); | ||
| $this->assertSame($this->sourceFile->statementList[0], $it->current()); | ||
| $it->next(); | ||
|
|
||
| // Token(kind=ScriptSectionStartTag) | ||
| $this->assertTrue($it->valid()); | ||
| $this->assertSame('scriptSectionStartTag', $it->key()); | ||
| $this->assertSame($this->sourceFile->statementList[0]->scriptSectionStartTag, $it->current()); | ||
| $it->next(); | ||
|
|
||
| // Token(kind=InlineHtml) | ||
| $this->assertTrue($it->valid()); | ||
| $this->assertSame('text', $it->key()); | ||
| $this->assertSame($this->sourceFile->statementList[0]->text, $it->current()); | ||
| $it->next(); | ||
|
|
||
| // Node\Statement\FunctionDeclaration | ||
| $this->assertTrue($it->valid()); | ||
| $this->assertSame('statementList', $it->key()); | ||
| $this->assertSame($this->sourceFile->statementList[1], $it->current()); | ||
| $it->next(); | ||
|
|
||
| // Token(kind=FunctionKeyword) | ||
| $this->assertTrue($it->valid()); | ||
| $this->assertSame('functionKeyword', $it->key()); | ||
| $this->assertSame($this->sourceFile->statementList[1]->functionKeyword, $it->current()); | ||
| $it->next(); | ||
|
|
||
| // Token(kind=Name) | ||
| $this->assertTrue($it->valid()); | ||
| $this->assertSame('name', $it->key()); | ||
| $this->assertSame($this->sourceFile->statementList[1]->name, $it->current()); | ||
| $it->next(); | ||
|
|
||
| // Token(kind=OpenParenToken) | ||
| $this->assertTrue($it->valid()); | ||
| $this->assertSame('openParen', $it->key()); | ||
| $this->assertSame($this->sourceFile->statementList[1]->openParen, $it->current()); | ||
| $it->next(); | ||
|
|
||
| // Token(kind=CloseParenToken) | ||
| $this->assertTrue($it->valid()); | ||
| $this->assertSame('closeParen', $it->key()); | ||
| $this->assertSame($this->sourceFile->statementList[1]->closeParen, $it->current()); | ||
| $it->next(); | ||
|
|
||
| // Node\Statement\CompoundStatementNode | ||
| $this->assertTrue($it->valid()); | ||
| $this->assertSame('compoundStatementOrSemicolon', $it->key()); | ||
| $this->assertSame($this->sourceFile->statementList[1]->compoundStatementOrSemicolon, $it->current()); | ||
| $it->next(); | ||
|
|
||
| // TODO finish | ||
| $this->markTestIncomplete(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo