-
Notifications
You must be signed in to change notification settings - Fork 80
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
Closed
Add NodeIterator #139
Changes from 10 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
99fd539
Add NodeIterator
felixfbecker 79e87c8
Fixes
felixfbecker dca82af
Fix InlineHtml order
felixfbecker 0f82f05
Fix typos
felixfbecker ba494d4
Complete test
felixfbecker aa0a707
Improve docs
felixfbecker fd6358f
Polish
felixfbecker 1cb1356
Add AncestorIterator
felixfbecker d63e978
Add docs for NodeAncestorIterator
felixfbecker d42d70b
Fix NodeAncestorIteratorTest
felixfbecker 6379ba9
Add perf test scripts
roblourens 23b9868
Fix NodeIterator namespace reference
roblourens 348abb9
Don't include parsing in benchmarks
felixfbecker 4ee5b75
Make faster
felixfbecker 035e87e
Can't help it
felixfbecker File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Should this be Iterator\NodeIterator?
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.
woops, yes. didn't change it when I moved it to the namespace.