diff --git a/lib/PhpParser/NodeTraverser.php b/lib/PhpParser/NodeTraverser.php index a238e28..6110488 100644 --- a/lib/PhpParser/NodeTraverser.php +++ b/lib/PhpParser/NodeTraverser.php @@ -13,6 +13,14 @@ class NodeTraverser implements NodeTraverserInterface */ const DONT_TRAVERSE_CHILDREN = 1; + /** + * If NodeVisitor::enterNode() or NodeVisitor::leaveNode() returns + * STOP_TRAVERSAL, traversal is aborted. + * + * The afterTraverse() method will still be invoked. + */ + const STOP_TRAVERSAL = 2; + /** * If NodeVisitor::leaveNode() returns REMOVE_NODE for a node that occurs * in an array, it will be removed from the array. @@ -25,6 +33,9 @@ class NodeTraverser implements NodeTraverserInterface /** @var NodeVisitor[] Visitors */ protected $visitors; + /** @var bool Whether traversal should be stopped */ + protected $stopTraversal; + /** * Constructs a node traverser. */ @@ -63,6 +74,8 @@ class NodeTraverser implements NodeTraverserInterface * @return Node[] Traversed array of nodes */ public function traverse(array $nodes) { + $this->stopTraversal = false; + foreach ($this->visitors as $visitor) { if (null !== $return = $visitor->beforeTraverse($nodes)) { $nodes = $return; @@ -93,12 +106,18 @@ class NodeTraverser implements NodeTraverserInterface if (is_array($subNode)) { $subNode = $this->traverseArray($subNode); + if ($this->stopTraversal) { + break; + } } elseif ($subNode instanceof Node) { $traverseChildren = true; foreach ($this->visitors as $visitor) { $return = $visitor->enterNode($subNode); if (self::DONT_TRAVERSE_CHILDREN === $return) { $traverseChildren = false; + } else if (self::STOP_TRAVERSAL === $return) { + $this->stopTraversal = true; + break 2; } else if (null !== $return) { $subNode = $return; } @@ -106,10 +125,17 @@ class NodeTraverser implements NodeTraverserInterface if ($traverseChildren) { $subNode = $this->traverseNode($subNode); + if ($this->stopTraversal) { + break; + } } foreach ($this->visitors as $visitor) { - if (null !== $return = $visitor->leaveNode($subNode)) { + $return = $visitor->leaveNode($subNode); + if (self::STOP_TRAVERSAL === $return) { + $this->stopTraversal = true; + break 2; + } else if (null !== $return) { if (is_array($return)) { throw new \LogicException( 'leaveNode() may only return an array ' . @@ -138,12 +164,18 @@ class NodeTraverser implements NodeTraverserInterface foreach ($nodes as $i => &$node) { if (is_array($node)) { $node = $this->traverseArray($node); + if ($this->stopTraversal) { + break; + } } elseif ($node instanceof Node) { $traverseChildren = true; foreach ($this->visitors as $visitor) { $return = $visitor->enterNode($node); if (self::DONT_TRAVERSE_CHILDREN === $return) { $traverseChildren = false; + } else if (self::STOP_TRAVERSAL === $return) { + $this->stopTraversal = true; + break 2; } else if (null !== $return) { $node = $return; } @@ -151,6 +183,9 @@ class NodeTraverser implements NodeTraverserInterface if ($traverseChildren) { $node = $this->traverseNode($node); + if ($this->stopTraversal) { + break; + } } foreach ($this->visitors as $visitor) { @@ -159,6 +194,9 @@ class NodeTraverser implements NodeTraverserInterface if (self::REMOVE_NODE === $return) { $doNodes[] = array($i, array()); break; + } else if (self::STOP_TRAVERSAL === $return) { + $this->stopTraversal = true; + break 2; } elseif (is_array($return)) { $doNodes[] = array($i, $return); break; diff --git a/lib/PhpParser/NodeVisitor.php b/lib/PhpParser/NodeVisitor.php index e7d9882..d56e6d7 100644 --- a/lib/PhpParser/NodeVisitor.php +++ b/lib/PhpParser/NodeVisitor.php @@ -25,12 +25,14 @@ interface NodeVisitor * => $node stays as-is * * NodeTraverser::DONT_TRAVERSE_CHILDREN * => Children of $node are not traversed. $node stays as-is + * * NodeTraverser::STOP_TRAVERSAL + * => Traversal is aborted. $node stays as-is * * otherwise * => $node is set to the return value * * @param Node $node Node * - * @return null|Node|int Replacement node (or special return value) + * @return null|int|Node Replacement node (or special return value) */ public function enterNode(Node $node); @@ -42,6 +44,8 @@ interface NodeVisitor * => $node stays as-is * * NodeTraverser::REMOVE_NODE * => $node is removed from the parent array + * * NodeTraverser::STOP_TRAVERSAL + * => Traversal is aborted. $node stays as-is * * array (of Nodes) * => The return value is merged into the parent array (at the position of the $node) * * otherwise @@ -49,7 +53,7 @@ interface NodeVisitor * * @param Node $node Node * - * @return null|Node|false|Node[] Replacement node (or special return value) + * @return null|false|int|Node|Node[] Replacement node (or special return value) */ public function leaveNode(Node $node); diff --git a/test/PhpParser/NodeTraverserTest.php b/test/PhpParser/NodeTraverserTest.php index 70f91b2..f4bade0 100644 --- a/test/PhpParser/NodeTraverserTest.php +++ b/test/PhpParser/NodeTraverserTest.php @@ -166,6 +166,63 @@ class NodeTraverserTest extends \PHPUnit_Framework_TestCase $this->assertEquals($stmts, $traverser->traverse($stmts)); } + public function testStopTraversal() { + $varNode1 = new Expr\Variable('a'); + $varNode2 = new Expr\Variable('b'); + $varNode3 = new Expr\Variable('c'); + $mulNode = new Expr\BinaryOp\Mul($varNode1, $varNode2); + $printNode = new Expr\Print_($varNode3); + $stmts = [$mulNode, $printNode]; + + // From enterNode() with array parent + $visitor = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); + $visitor->expects($this->at(1))->method('enterNode')->with($mulNode) + ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL)); + $visitor->expects($this->at(2))->method('afterTraversal'); + $traverser = new NodeTraverser; + $traverser->addVisitor($visitor); + $this->assertEquals($stmts, $traverser->traverse($stmts)); + + // From enterNode with Node parent + $visitor = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); + $visitor->expects($this->at(2))->method('enterNode')->with($varNode1) + ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL)); + $visitor->expects($this->at(3))->method('afterTraversal'); + $traverser = new NodeTraverser; + $traverser->addVisitor($visitor); + $this->assertEquals($stmts, $traverser->traverse($stmts)); + + // From leaveNode with Node parent + $visitor = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); + $visitor->expects($this->at(3))->method('leaveNode')->with($varNode1) + ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL)); + $visitor->expects($this->at(4))->method('afterTraversal'); + $traverser = new NodeTraverser; + $traverser->addVisitor($visitor); + $this->assertEquals($stmts, $traverser->traverse($stmts)); + + // From leaveNode with array parent + $visitor = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); + $visitor->expects($this->at(6))->method('leaveNode')->with($mulNode) + ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL)); + $visitor->expects($this->at(7))->method('afterTraversal'); + $traverser = new NodeTraverser; + $traverser->addVisitor($visitor); + $this->assertEquals($stmts, $traverser->traverse($stmts)); + + // Check that pending array modifications are still carried out + $visitor = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); + $visitor->expects($this->at(6))->method('leaveNode')->with($mulNode) + ->will($this->returnValue(NodeTraverser::REMOVE_NODE)); + $visitor->expects($this->at(7))->method('enterNode')->with($printNode) + ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL)); + $visitor->expects($this->at(8))->method('afterTraversal'); + $traverser = new NodeTraverser; + $traverser->addVisitor($visitor); + $this->assertEquals([$printNode], $traverser->traverse($stmts)); + + } + public function testRemovingVisitor() { $visitor1 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $visitor2 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock();