NodeTraverserTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <?php declare(strict_types=1);
  2. namespace PhpParser;
  3. use PhpParser\Node\Expr;
  4. use PhpParser\Node\Scalar\String_;
  5. use PhpParser\NodeVisitor;
  6. use PHPUnit\Framework\TestCase;
  7. class NodeTraverserTest extends TestCase
  8. {
  9. public function testNonModifying() {
  10. $str1Node = new String_('Foo');
  11. $str2Node = new String_('Bar');
  12. $echoNode = new Node\Stmt\Echo_([$str1Node, $str2Node]);
  13. $stmts = [$echoNode];
  14. $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
  15. $visitor->expects($this->at(0))->method('beforeTraverse')->with($stmts);
  16. $visitor->expects($this->at(1))->method('enterNode')->with($echoNode);
  17. $visitor->expects($this->at(2))->method('enterNode')->with($str1Node);
  18. $visitor->expects($this->at(3))->method('leaveNode')->with($str1Node);
  19. $visitor->expects($this->at(4))->method('enterNode')->with($str2Node);
  20. $visitor->expects($this->at(5))->method('leaveNode')->with($str2Node);
  21. $visitor->expects($this->at(6))->method('leaveNode')->with($echoNode);
  22. $visitor->expects($this->at(7))->method('afterTraverse')->with($stmts);
  23. $traverser = new NodeTraverser;
  24. $traverser->addVisitor($visitor);
  25. $this->assertEquals($stmts, $traverser->traverse($stmts));
  26. }
  27. public function testModifying() {
  28. $str1Node = new String_('Foo');
  29. $str2Node = new String_('Bar');
  30. $printNode = new Expr\Print_($str1Node);
  31. // first visitor changes the node, second verifies the change
  32. $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  33. $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  34. // replace empty statements with string1 node
  35. $visitor1->expects($this->at(0))->method('beforeTraverse')->with([])
  36. ->will($this->returnValue([$str1Node]));
  37. $visitor2->expects($this->at(0))->method('beforeTraverse')->with([$str1Node]);
  38. // replace string1 node with print node
  39. $visitor1->expects($this->at(1))->method('enterNode')->with($str1Node)
  40. ->will($this->returnValue($printNode));
  41. $visitor2->expects($this->at(1))->method('enterNode')->with($printNode);
  42. // replace string1 node with string2 node
  43. $visitor1->expects($this->at(2))->method('enterNode')->with($str1Node)
  44. ->will($this->returnValue($str2Node));
  45. $visitor2->expects($this->at(2))->method('enterNode')->with($str2Node);
  46. // replace string2 node with string1 node again
  47. $visitor1->expects($this->at(3))->method('leaveNode')->with($str2Node)
  48. ->will($this->returnValue($str1Node));
  49. $visitor2->expects($this->at(3))->method('leaveNode')->with($str1Node);
  50. // replace print node with string1 node again
  51. $visitor1->expects($this->at(4))->method('leaveNode')->with($printNode)
  52. ->will($this->returnValue($str1Node));
  53. $visitor2->expects($this->at(4))->method('leaveNode')->with($str1Node);
  54. // replace string1 node with empty statements again
  55. $visitor1->expects($this->at(5))->method('afterTraverse')->with([$str1Node])
  56. ->will($this->returnValue([]));
  57. $visitor2->expects($this->at(5))->method('afterTraverse')->with([]);
  58. $traverser = new NodeTraverser;
  59. $traverser->addVisitor($visitor1);
  60. $traverser->addVisitor($visitor2);
  61. // as all operations are reversed we end where we start
  62. $this->assertEquals([], $traverser->traverse([]));
  63. }
  64. public function testRemove() {
  65. $str1Node = new String_('Foo');
  66. $str2Node = new String_('Bar');
  67. $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
  68. // remove the string1 node, leave the string2 node
  69. $visitor->expects($this->at(2))->method('leaveNode')->with($str1Node)
  70. ->will($this->returnValue(NodeTraverser::REMOVE_NODE));
  71. $traverser = new NodeTraverser;
  72. $traverser->addVisitor($visitor);
  73. $this->assertEquals([$str2Node], $traverser->traverse([$str1Node, $str2Node]));
  74. }
  75. public function testMerge() {
  76. $strStart = new String_('Start');
  77. $strMiddle = new String_('End');
  78. $strEnd = new String_('Middle');
  79. $strR1 = new String_('Replacement 1');
  80. $strR2 = new String_('Replacement 2');
  81. $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
  82. // replace strMiddle with strR1 and strR2 by merge
  83. $visitor->expects($this->at(4))->method('leaveNode')->with($strMiddle)
  84. ->will($this->returnValue([$strR1, $strR2]));
  85. $traverser = new NodeTraverser;
  86. $traverser->addVisitor($visitor);
  87. $this->assertEquals(
  88. [$strStart, $strR1, $strR2, $strEnd],
  89. $traverser->traverse([$strStart, $strMiddle, $strEnd])
  90. );
  91. }
  92. public function testInvalidDeepArray() {
  93. $this->expectException(\LogicException::class);
  94. $this->expectExceptionMessage('Invalid node structure: Contains nested arrays');
  95. $strNode = new String_('Foo');
  96. $stmts = [[[$strNode]]];
  97. $traverser = new NodeTraverser;
  98. $this->assertEquals($stmts, $traverser->traverse($stmts));
  99. }
  100. public function testDontTraverseChildren() {
  101. $strNode = new String_('str');
  102. $printNode = new Expr\Print_($strNode);
  103. $varNode = new Expr\Variable('foo');
  104. $mulNode = new Expr\BinaryOp\Mul($varNode, $varNode);
  105. $negNode = new Expr\UnaryMinus($mulNode);
  106. $stmts = [$printNode, $negNode];
  107. $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  108. $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  109. $visitor1->expects($this->at(1))->method('enterNode')->with($printNode)
  110. ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CHILDREN));
  111. $visitor2->expects($this->at(1))->method('enterNode')->with($printNode);
  112. $visitor1->expects($this->at(2))->method('leaveNode')->with($printNode);
  113. $visitor2->expects($this->at(2))->method('leaveNode')->with($printNode);
  114. $visitor1->expects($this->at(3))->method('enterNode')->with($negNode);
  115. $visitor2->expects($this->at(3))->method('enterNode')->with($negNode);
  116. $visitor1->expects($this->at(4))->method('enterNode')->with($mulNode);
  117. $visitor2->expects($this->at(4))->method('enterNode')->with($mulNode)
  118. ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CHILDREN));
  119. $visitor1->expects($this->at(5))->method('leaveNode')->with($mulNode);
  120. $visitor2->expects($this->at(5))->method('leaveNode')->with($mulNode);
  121. $visitor1->expects($this->at(6))->method('leaveNode')->with($negNode);
  122. $visitor2->expects($this->at(6))->method('leaveNode')->with($negNode);
  123. $traverser = new NodeTraverser;
  124. $traverser->addVisitor($visitor1);
  125. $traverser->addVisitor($visitor2);
  126. $this->assertEquals($stmts, $traverser->traverse($stmts));
  127. }
  128. public function testDontTraverseCurrentAndChildren() {
  129. // print 'str'; -($foo * $foo);
  130. $strNode = new String_('str');
  131. $printNode = new Expr\Print_($strNode);
  132. $varNode = new Expr\Variable('foo');
  133. $mulNode = new Expr\BinaryOp\Mul($varNode, $varNode);
  134. $divNode = new Expr\BinaryOp\Div($varNode, $varNode);
  135. $negNode = new Expr\UnaryMinus($mulNode);
  136. $stmts = [$printNode, $negNode];
  137. $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  138. $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  139. $visitor1->expects($this->at(1))->method('enterNode')->with($printNode)
  140. ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN));
  141. $visitor1->expects($this->at(2))->method('leaveNode')->with($printNode);
  142. $visitor1->expects($this->at(3))->method('enterNode')->with($negNode);
  143. $visitor2->expects($this->at(1))->method('enterNode')->with($negNode);
  144. $visitor1->expects($this->at(4))->method('enterNode')->with($mulNode)
  145. ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN));
  146. $visitor1->expects($this->at(5))->method('leaveNode')->with($mulNode)->willReturn($divNode);
  147. $visitor1->expects($this->at(6))->method('leaveNode')->with($negNode);
  148. $visitor2->expects($this->at(2))->method('leaveNode')->with($negNode);
  149. $traverser = new NodeTraverser;
  150. $traverser->addVisitor($visitor1);
  151. $traverser->addVisitor($visitor2);
  152. $resultStmts = $traverser->traverse($stmts);
  153. $this->assertInstanceOf(Expr\BinaryOp\Div::class, $resultStmts[1]->expr);
  154. }
  155. public function testStopTraversal() {
  156. $varNode1 = new Expr\Variable('a');
  157. $varNode2 = new Expr\Variable('b');
  158. $varNode3 = new Expr\Variable('c');
  159. $mulNode = new Expr\BinaryOp\Mul($varNode1, $varNode2);
  160. $printNode = new Expr\Print_($varNode3);
  161. $stmts = [$mulNode, $printNode];
  162. // From enterNode() with array parent
  163. $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
  164. $visitor->expects($this->at(1))->method('enterNode')->with($mulNode)
  165. ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
  166. $visitor->expects($this->at(2))->method('afterTraverse');
  167. $traverser = new NodeTraverser;
  168. $traverser->addVisitor($visitor);
  169. $this->assertEquals($stmts, $traverser->traverse($stmts));
  170. // From enterNode with Node parent
  171. $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
  172. $visitor->expects($this->at(2))->method('enterNode')->with($varNode1)
  173. ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
  174. $visitor->expects($this->at(3))->method('afterTraverse');
  175. $traverser = new NodeTraverser;
  176. $traverser->addVisitor($visitor);
  177. $this->assertEquals($stmts, $traverser->traverse($stmts));
  178. // From leaveNode with Node parent
  179. $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
  180. $visitor->expects($this->at(3))->method('leaveNode')->with($varNode1)
  181. ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
  182. $visitor->expects($this->at(4))->method('afterTraverse');
  183. $traverser = new NodeTraverser;
  184. $traverser->addVisitor($visitor);
  185. $this->assertEquals($stmts, $traverser->traverse($stmts));
  186. // From leaveNode with array parent
  187. $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
  188. $visitor->expects($this->at(6))->method('leaveNode')->with($mulNode)
  189. ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
  190. $visitor->expects($this->at(7))->method('afterTraverse');
  191. $traverser = new NodeTraverser;
  192. $traverser->addVisitor($visitor);
  193. $this->assertEquals($stmts, $traverser->traverse($stmts));
  194. // Check that pending array modifications are still carried out
  195. $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
  196. $visitor->expects($this->at(6))->method('leaveNode')->with($mulNode)
  197. ->will($this->returnValue(NodeTraverser::REMOVE_NODE));
  198. $visitor->expects($this->at(7))->method('enterNode')->with($printNode)
  199. ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
  200. $visitor->expects($this->at(8))->method('afterTraverse');
  201. $traverser = new NodeTraverser;
  202. $traverser->addVisitor($visitor);
  203. $this->assertEquals([$printNode], $traverser->traverse($stmts));
  204. }
  205. public function testRemovingVisitor() {
  206. $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  207. $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  208. $visitor3 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  209. $traverser = new NodeTraverser;
  210. $traverser->addVisitor($visitor1);
  211. $traverser->addVisitor($visitor2);
  212. $traverser->addVisitor($visitor3);
  213. $preExpected = [$visitor1, $visitor2, $visitor3];
  214. $this->assertAttributeSame($preExpected, 'visitors', $traverser, 'The appropriate visitors have not been added');
  215. $traverser->removeVisitor($visitor2);
  216. $postExpected = [0 => $visitor1, 2 => $visitor3];
  217. $this->assertAttributeSame($postExpected, 'visitors', $traverser, 'The appropriate visitors are not present after removal');
  218. }
  219. public function testNoCloneNodes() {
  220. $stmts = [new Node\Stmt\Echo_([new String_('Foo'), new String_('Bar')])];
  221. $traverser = new NodeTraverser;
  222. $this->assertSame($stmts, $traverser->traverse($stmts));
  223. }
  224. /**
  225. * @dataProvider provideTestInvalidReturn
  226. */
  227. public function testInvalidReturn($visitor, $message) {
  228. $this->expectException(\LogicException::class);
  229. $this->expectExceptionMessage($message);
  230. $stmts = [new Node\Stmt\Expression(new Node\Scalar\LNumber(42))];
  231. $traverser = new NodeTraverser();
  232. $traverser->addVisitor($visitor);
  233. $traverser->traverse($stmts);
  234. }
  235. public function provideTestInvalidReturn() {
  236. $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  237. $visitor1->expects($this->at(1))->method('enterNode')
  238. ->willReturn('foobar');
  239. $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  240. $visitor2->expects($this->at(2))->method('enterNode')
  241. ->willReturn('foobar');
  242. $visitor3 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  243. $visitor3->expects($this->at(3))->method('leaveNode')
  244. ->willReturn('foobar');
  245. $visitor4 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  246. $visitor4->expects($this->at(4))->method('leaveNode')
  247. ->willReturn('foobar');
  248. $visitor5 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  249. $visitor5->expects($this->at(3))->method('leaveNode')
  250. ->willReturn([new Node\Scalar\DNumber(42.0)]);
  251. $visitor6 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  252. $visitor6->expects($this->at(4))->method('leaveNode')
  253. ->willReturn(false);
  254. $visitor7 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  255. $visitor7->expects($this->at(1))->method('enterNode')
  256. ->willReturn(new Node\Scalar\LNumber(42));
  257. $visitor8 = $this->getMockBuilder(NodeVisitor::class)->getMock();
  258. $visitor8->expects($this->at(2))->method('enterNode')
  259. ->willReturn(new Node\Stmt\Return_());
  260. return [
  261. [$visitor1, 'enterNode() returned invalid value of type string'],
  262. [$visitor2, 'enterNode() returned invalid value of type string'],
  263. [$visitor3, 'leaveNode() returned invalid value of type string'],
  264. [$visitor4, 'leaveNode() returned invalid value of type string'],
  265. [$visitor5, 'leaveNode() may only return an array if the parent structure is an array'],
  266. [$visitor6, 'bool(false) return from leaveNode() no longer supported. Return NodeTraverser::REMOVE_NODE instead'],
  267. [$visitor7, 'Trying to replace statement (Stmt_Expression) with expression (Scalar_LNumber). Are you missing a Stmt_Expression wrapper?'],
  268. [$visitor8, 'Trying to replace expression (Scalar_LNumber) with statement (Stmt_Return)'],
  269. ];
  270. }
  271. }