diff --git a/doc/2_Usage_of_basic_components.markdown b/doc/2_Usage_of_basic_components.markdown index d4ea040..e9db1d6 100644 --- a/doc/2_Usage_of_basic_components.markdown +++ b/doc/2_Usage_of_basic_components.markdown @@ -241,23 +241,27 @@ methods: public function leaveNode(PhpParser\Node $node); public function afterTraverse(array $nodes); -The `beforeTraverse` method is called once before the traversal begins and is passed the nodes the +The `beforeTraverse()` method is called once before the traversal begins and is passed the nodes the traverser was called with. This method can be used for resetting values before traversation or preparing the tree for traversal. -The `afterTraverse` method is similar to the `beforeTraverse` method, with the only difference that +The `afterTraverse()` method is similar to the `beforeTraverse()` method, with the only difference that it is called once after the traversal. -The `enterNode` and `leaveNode` methods are called on every node, the former when it is entered, +The `enterNode()` and `leaveNode()` methods are called on every node, the former when it is entered, i.e. before its subnodes are traversed, the latter when it is left. All four methods can either return the changed node or not return at all (i.e. `null`) in which -case the current node is not changed. The `leaveNode` method can additionally return two special -values: +case the current node is not changed. -If `false` is returned the current node will be removed from the parent array. If an array is returned -it will be merged into the parent array at the offset of the current node. I.e. if in `array(A, B, C)` -the node `B` should be replaced with `array(X, Y, Z)` the result will be `array(A, X, Y, Z, C)`. +The `enterNode()` method can additionally return the value `NodeTraverser::DONT_TRAVERSE_CHILDREN`, +which instructs the traverser to skip all children of the current node. + +The `leaveNode()` method can additionally return the value `NodeTraverser::REMOVE_NODE`, in which +case the current node will be removed from the parent array. Furthermove it is possible to return +an array of nodes, which will be merged into the parent array at the offset of the current node. +I.e. if in `array(A, B, C)` the node `B` should be replaced with `array(X, Y, Z)` the result will +be `array(A, X, Y, Z, C)`. Instead of manually implementing the `NodeVisitor` interface you can also extend the `NodeVisitorAbstract` class, which will define empty default implementations for all the above methods. diff --git a/lib/PhpParser/NodeTraverser.php b/lib/PhpParser/NodeTraverser.php index fb0c22b..6f8a328 100644 --- a/lib/PhpParser/NodeTraverser.php +++ b/lib/PhpParser/NodeTraverser.php @@ -73,13 +73,19 @@ class NodeTraverser implements NodeTraverserInterface if (is_array($subNode)) { $subNode = $this->traverseArray($subNode); } elseif ($subNode instanceof Node) { + $traverseChildren = true; foreach ($this->visitors as $visitor) { - if (null !== $return = $visitor->enterNode($subNode)) { + $return = $visitor->enterNode($subNode); + if (self::DONT_TRAVERSE_CHILDREN === $return) { + $traverseChildren = false; + } else if (null !== $return) { $subNode = $return; } } - $subNode = $this->traverseNode($subNode); + if ($traverseChildren) { + $subNode = $this->traverseNode($subNode); + } foreach ($this->visitors as $visitor) { if (null !== $return = $visitor->leaveNode($subNode)) { @@ -99,18 +105,24 @@ class NodeTraverser implements NodeTraverserInterface if (is_array($node)) { $node = $this->traverseArray($node); } elseif ($node instanceof Node) { + $traverseChildren = true; foreach ($this->visitors as $visitor) { - if (null !== $return = $visitor->enterNode($node)) { + $return = $visitor->enterNode($node); + if (self::DONT_TRAVERSE_CHILDREN === $return) { + $traverseChildren = false; + } else if (null !== $return) { $node = $return; } } - $node = $this->traverseNode($node); + if ($traverseChildren) { + $node = $this->traverseNode($node); + } foreach ($this->visitors as $visitor) { $return = $visitor->leaveNode($node); - if (false === $return) { + if (self::REMOVE_NODE === $return) { $doNodes[] = array($i, array()); break; } elseif (is_array($return)) { @@ -131,4 +143,4 @@ class NodeTraverser implements NodeTraverserInterface return $nodes; } -} \ No newline at end of file +} diff --git a/lib/PhpParser/NodeTraverserInterface.php b/lib/PhpParser/NodeTraverserInterface.php index 0f88e46..0752de2 100644 --- a/lib/PhpParser/NodeTraverserInterface.php +++ b/lib/PhpParser/NodeTraverserInterface.php @@ -4,6 +4,24 @@ namespace PhpParser; interface NodeTraverserInterface { + /** + * If NodeVisitor::enterNode() returns DONT_TRAVERSE_CHILDREN, child nodes + * of the current node will not be traversed for any visitors. + * + * For subsequent visitors enterNode() will still be called on the current + * node and leaveNode() will also be invoked for the current node. + */ + const DONT_TRAVERSE_CHILDREN = 1; + + /** + * If NodeVisitor::leaveNode() returns REMOVE_NODE for a node that occurs + * in an array, it will be removed from the array. + * + * For subsequent visitors leaveNode() will still be invoked for the + * removed node. + */ + const REMOVE_NODE = false; + /** * Adds a visitor. * diff --git a/test/PhpParser/NodeTraverserTest.php b/test/PhpParser/NodeTraverserTest.php index 757dfde..7acb4d1 100644 --- a/test/PhpParser/NodeTraverserTest.php +++ b/test/PhpParser/NodeTraverserTest.php @@ -3,6 +3,7 @@ namespace PhpParser; use PhpParser\Node\Scalar\String; +use PhpParser\Node\Expr; class NodeTraverserTest extends \PHPUnit_Framework_TestCase { @@ -32,7 +33,7 @@ class NodeTraverserTest extends \PHPUnit_Framework_TestCase public function testModifying() { $str1Node = new String('Foo'); $str2Node = new String('Bar'); - $printNode = new Node\Expr\Print_($str1Node); + $printNode = new Expr\Print_($str1Node); // first visitor changes the node, second verifies the change $visitor1 = $this->getMock('PhpParser\NodeVisitor'); @@ -127,6 +128,43 @@ class NodeTraverserTest extends \PHPUnit_Framework_TestCase $this->assertEquals($stmts, $traverser->traverse($stmts)); } + public function testDontTraverseChildren() { + $strNode = new String('str'); + $printNode = new Expr\Print_($strNode); + $argNode = new Node\Arg($strNode); + $callNode = new Expr\FuncCall(new Node\Name('test'), array($argNode)); + $stmts = array($printNode, $callNode); + + $visitor1 = $this->getMock('PhpParser\NodeVisitor'); + $visitor2 = $this->getMock('PhpParser\NodeVisitor'); + + $visitor1->expects($this->at(1))->method('enterNode')->with($printNode) + ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CHILDREN)); + $visitor2->expects($this->at(1))->method('enterNode')->with($printNode); + + $visitor1->expects($this->at(2))->method('leaveNode')->with($printNode); + $visitor2->expects($this->at(2))->method('leaveNode')->with($printNode); + + $visitor1->expects($this->at(3))->method('enterNode')->with($callNode); + $visitor2->expects($this->at(3))->method('enterNode')->with($callNode); + + $visitor1->expects($this->at(6))->method('enterNode')->with($argNode); + $visitor2->expects($this->at(6))->method('enterNode')->with($argNode) + ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CHILDREN)); + + $visitor1->expects($this->at(7))->method('leaveNode')->with($argNode); + $visitor2->expects($this->at(7))->method('leaveNode')->with($argNode); + + $visitor1->expects($this->at(8))->method('leaveNode')->with($callNode); + $visitor2->expects($this->at(8))->method('leaveNode')->with($callNode); + + $traverser = new NodeTraverser; + $traverser->addVisitor($visitor1); + $traverser->addVisitor($visitor2); + + $this->assertEquals($stmts, $traverser->traverse($stmts)); + } + public function testRemovingVisitor() { $visitor1 = $this->getMock('PhpParser\NodeVisitor'); $visitor2 = $this->getMock('PhpParser\NodeVisitor'); @@ -145,4 +183,4 @@ class NodeTraverserTest extends \PHPUnit_Framework_TestCase $postExpected = array(0 => $visitor1, 2 => $visitor3); $this->assertAttributeSame($postExpected, 'visitors', $traverser, 'The appropriate visitors are not present after removal'); } -} \ No newline at end of file +}