<?php namespace PhpParser; use PhpParser\Node\Expr; use PhpParser\Node\Scalar\String_; use PHPUnit\Framework\TestCase; class NodeTraverserTest extends TestCase { public function testNonModifying() { $str1Node = new String_('Foo'); $str2Node = new String_('Bar'); $echoNode = new Node\Stmt\Echo_([$str1Node, $str2Node]); $stmts = [$echoNode]; $visitor = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $visitor->expects($this->at(0))->method('beforeTraverse')->with($stmts); $visitor->expects($this->at(1))->method('enterNode')->with($echoNode); $visitor->expects($this->at(2))->method('enterNode')->with($str1Node); $visitor->expects($this->at(3))->method('leaveNode')->with($str1Node); $visitor->expects($this->at(4))->method('enterNode')->with($str2Node); $visitor->expects($this->at(5))->method('leaveNode')->with($str2Node); $visitor->expects($this->at(6))->method('leaveNode')->with($echoNode); $visitor->expects($this->at(7))->method('afterTraverse')->with($stmts); $traverser = new NodeTraverser; $traverser->addVisitor($visitor); $this->assertEquals($stmts, $traverser->traverse($stmts)); } public function testModifying() { $str1Node = new String_('Foo'); $str2Node = new String_('Bar'); $printNode = new Expr\Print_($str1Node); // first visitor changes the node, second verifies the change $visitor1 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $visitor2 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); // replace empty statements with string1 node $visitor1->expects($this->at(0))->method('beforeTraverse')->with([]) ->will($this->returnValue([$str1Node])); $visitor2->expects($this->at(0))->method('beforeTraverse')->with([$str1Node]); // replace string1 node with print node $visitor1->expects($this->at(1))->method('enterNode')->with($str1Node) ->will($this->returnValue($printNode)); $visitor2->expects($this->at(1))->method('enterNode')->with($printNode); // replace string1 node with string2 node $visitor1->expects($this->at(2))->method('enterNode')->with($str1Node) ->will($this->returnValue($str2Node)); $visitor2->expects($this->at(2))->method('enterNode')->with($str2Node); // replace string2 node with string1 node again $visitor1->expects($this->at(3))->method('leaveNode')->with($str2Node) ->will($this->returnValue($str1Node)); $visitor2->expects($this->at(3))->method('leaveNode')->with($str1Node); // replace print node with string1 node again $visitor1->expects($this->at(4))->method('leaveNode')->with($printNode) ->will($this->returnValue($str1Node)); $visitor2->expects($this->at(4))->method('leaveNode')->with($str1Node); // replace string1 node with empty statements again $visitor1->expects($this->at(5))->method('afterTraverse')->with([$str1Node]) ->will($this->returnValue([])); $visitor2->expects($this->at(5))->method('afterTraverse')->with([]); $traverser = new NodeTraverser; $traverser->addVisitor($visitor1); $traverser->addVisitor($visitor2); // as all operations are reversed we end where we start $this->assertEquals([], $traverser->traverse([])); } public function testRemove() { $str1Node = new String_('Foo'); $str2Node = new String_('Bar'); $visitor = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); // remove the string1 node, leave the string2 node $visitor->expects($this->at(2))->method('leaveNode')->with($str1Node) ->will($this->returnValue(NodeTraverser::REMOVE_NODE)); $traverser = new NodeTraverser; $traverser->addVisitor($visitor); $this->assertEquals([$str2Node], $traverser->traverse([$str1Node, $str2Node])); } public function testMerge() { $strStart = new String_('Start'); $strMiddle = new String_('End'); $strEnd = new String_('Middle'); $strR1 = new String_('Replacement 1'); $strR2 = new String_('Replacement 2'); $visitor = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); // replace strMiddle with strR1 and strR2 by merge $visitor->expects($this->at(4))->method('leaveNode')->with($strMiddle) ->will($this->returnValue([$strR1, $strR2])); $traverser = new NodeTraverser; $traverser->addVisitor($visitor); $this->assertEquals( [$strStart, $strR1, $strR2, $strEnd], $traverser->traverse([$strStart, $strMiddle, $strEnd]) ); } /** * @expectedException \LogicException * @expectedExceptionMessage Invalid node structure: Contains nested arrays */ public function testInvalidDeepArray() { $strNode = new String_('Foo'); $stmts = [[[$strNode]]]; $traverser = new NodeTraverser; $this->assertEquals($stmts, $traverser->traverse($stmts)); } public function testDontTraverseChildren() { $strNode = new String_('str'); $printNode = new Expr\Print_($strNode); $varNode = new Expr\Variable('foo'); $mulNode = new Expr\BinaryOp\Mul($varNode, $varNode); $negNode = new Expr\UnaryMinus($mulNode); $stmts = [$printNode, $negNode]; $visitor1 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $visitor2 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $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($negNode); $visitor2->expects($this->at(3))->method('enterNode')->with($negNode); $visitor1->expects($this->at(4))->method('enterNode')->with($mulNode); $visitor2->expects($this->at(4))->method('enterNode')->with($mulNode) ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CHILDREN)); $visitor1->expects($this->at(5))->method('leaveNode')->with($mulNode); $visitor2->expects($this->at(5))->method('leaveNode')->with($mulNode); $visitor1->expects($this->at(6))->method('leaveNode')->with($negNode); $visitor2->expects($this->at(6))->method('leaveNode')->with($negNode); $traverser = new NodeTraverser; $traverser->addVisitor($visitor1); $traverser->addVisitor($visitor2); $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('afterTraverse'); $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('afterTraverse'); $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('afterTraverse'); $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('afterTraverse'); $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('afterTraverse'); $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(); $visitor3 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $traverser = new NodeTraverser; $traverser->addVisitor($visitor1); $traverser->addVisitor($visitor2); $traverser->addVisitor($visitor3); $preExpected = [$visitor1, $visitor2, $visitor3]; $this->assertAttributeSame($preExpected, 'visitors', $traverser, 'The appropriate visitors have not been added'); $traverser->removeVisitor($visitor2); $postExpected = [0 => $visitor1, 2 => $visitor3]; $this->assertAttributeSame($postExpected, 'visitors', $traverser, 'The appropriate visitors are not present after removal'); } public function testNoCloneNodes() { $stmts = [new Node\Stmt\Echo_([new String_('Foo'), new String_('Bar')])]; $traverser = new NodeTraverser; $this->assertSame($stmts, $traverser->traverse($stmts)); } /** * @dataProvider provideTestInvalidReturn */ public function testInvalidReturn($visitor, $message) { $this->expectException(\LogicException::class); $this->expectExceptionMessage($message); $stmts = [new Node\Expr\UnaryMinus(new Node\Scalar\LNumber(42))]; $traverser = new NodeTraverser(); $traverser->addVisitor($visitor); $traverser->traverse($stmts); } public function provideTestInvalidReturn() { $visitor1 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $visitor1->expects($this->at(1))->method('enterNode') ->will($this->returnValue('foobar')); $visitor2 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $visitor2->expects($this->at(2))->method('enterNode') ->will($this->returnValue('foobar')); $visitor3 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $visitor3->expects($this->at(3))->method('leaveNode') ->will($this->returnValue('foobar')); $visitor4 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $visitor4->expects($this->at(4))->method('leaveNode') ->will($this->returnValue('foobar')); $visitor5 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $visitor5->expects($this->at(3))->method('leaveNode') ->willReturn([new Node\Scalar\DNumber(42.0)]); $visitor6 = $this->getMockBuilder('PhpParser\NodeVisitor')->getMock(); $visitor6->expects($this->at(4))->method('leaveNode') ->willReturn(false); return [ [$visitor1, 'enterNode() returned invalid value of type string'], [$visitor2, 'enterNode() returned invalid value of type string'], [$visitor3, 'leaveNode() returned invalid value of type string'], [$visitor4, 'leaveNode() returned invalid value of type string'], [$visitor5, 'leaveNode() may only return an array if the parent structure is an array'], [$visitor6, 'bool(false) return from leaveNode() no longer supported. Return NodeTraverser::REMOVE_NODE instead'], ]; } }