NodeTraverserTest.php 13 KB

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