-
Notifications
You must be signed in to change notification settings - Fork 82
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 1 commit
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,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). | ||
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 |
|---|---|---|
|
|
@@ -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. | ||
| * | ||
|
|
||
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,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()); | ||
| } | ||
| } |
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,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 | ||
| ); | ||
| } | ||
| } |
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.
typo