NodeTraverserTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. <?php declare(strict_types=1);
  2. namespace PhpParser;
  3. use PhpParser\Node\Expr;
  4. use PhpParser\Node\Scalar\Int_;
  5. use PhpParser\Node\Scalar\String_;
  6. use PhpParser\Node\Stmt\Else_;
  7. use PhpParser\Node\Stmt\If_;
  8. class NodeTraverserTest extends \PHPUnit\Framework\TestCase {
  9. public function testNonModifying(): void {
  10. $str1Node = new String_('Foo');
  11. $str2Node = new String_('Bar');
  12. $echoNode = new Node\Stmt\Echo_([$str1Node, $str2Node]);
  13. $stmts = [$echoNode];
  14. $visitor = new NodeVisitorForTesting();
  15. $traverser = new NodeTraverser();
  16. $traverser->addVisitor($visitor);
  17. $this->assertEquals($stmts, $traverser->traverse($stmts));
  18. $this->assertEquals([
  19. ['beforeTraverse', $stmts],
  20. ['enterNode', $echoNode],
  21. ['enterNode', $str1Node],
  22. ['leaveNode', $str1Node],
  23. ['enterNode', $str2Node],
  24. ['leaveNode', $str2Node],
  25. ['leaveNode', $echoNode],
  26. ['afterTraverse', $stmts],
  27. ], $visitor->trace);
  28. }
  29. public function testModifying(): void {
  30. $str1Node = new String_('Foo');
  31. $str2Node = new String_('Bar');
  32. $printNode = new Expr\Print_($str1Node);
  33. // Visitor 2 performs changes, visitors 1 and 3 observe the changes.
  34. $visitor1 = new NodeVisitorForTesting();
  35. $visitor2 = new NodeVisitorForTesting([
  36. ['beforeTraverse', [], [$str1Node]],
  37. ['enterNode', $str1Node, $printNode],
  38. ['enterNode', $str1Node, $str2Node],
  39. ['leaveNode', $str2Node, $str1Node],
  40. ['leaveNode', $printNode, $str1Node],
  41. ['afterTraverse', [$str1Node], []],
  42. ]);
  43. $visitor3 = new NodeVisitorForTesting();
  44. $traverser = new NodeTraverser($visitor1, $visitor2, $visitor3);
  45. // as all operations are reversed we end where we start
  46. $this->assertEquals([], $traverser->traverse([]));
  47. $this->assertEquals([
  48. // Sees nodes before changes on entry.
  49. ['beforeTraverse', []],
  50. ['enterNode', $str1Node],
  51. ['enterNode', $str1Node],
  52. // Sees nodes after changes on leave.
  53. ['leaveNode', $str1Node],
  54. ['leaveNode', $str1Node],
  55. ['afterTraverse', []],
  56. ], $visitor1->trace);
  57. $this->assertEquals([
  58. // Sees nodes after changes on entry.
  59. ['beforeTraverse', [$str1Node]],
  60. ['enterNode', $printNode],
  61. ['enterNode', $str2Node],
  62. // Sees nodes before changes on leave.
  63. ['leaveNode', $str2Node],
  64. ['leaveNode', $printNode],
  65. ['afterTraverse', [$str1Node]],
  66. ], $visitor3->trace);
  67. }
  68. public function testRemoveFromLeave(): void {
  69. $str1Node = new String_('Foo');
  70. $str2Node = new String_('Bar');
  71. $visitor = new NodeVisitorForTesting([
  72. ['leaveNode', $str1Node, NodeVisitor::REMOVE_NODE],
  73. ]);
  74. $visitor2 = new NodeVisitorForTesting();
  75. $traverser = new NodeTraverser();
  76. $traverser->addVisitor($visitor2);
  77. $traverser->addVisitor($visitor);
  78. $stmts = [$str1Node, $str2Node];
  79. $this->assertEquals([$str2Node], $traverser->traverse($stmts));
  80. $this->assertEquals([
  81. ['beforeTraverse', $stmts],
  82. ['enterNode', $str1Node],
  83. ['enterNode', $str2Node],
  84. ['leaveNode', $str2Node],
  85. ['afterTraverse', [$str2Node]],
  86. ], $visitor2->trace);
  87. }
  88. public function testRemoveFromEnter(): void {
  89. $str1Node = new String_('Foo');
  90. $str2Node = new String_('Bar');
  91. $visitor = new NodeVisitorForTesting([
  92. ['enterNode', $str1Node, NodeVisitor::REMOVE_NODE],
  93. ]);
  94. $visitor2 = new NodeVisitorForTesting();
  95. $traverser = new NodeTraverser();
  96. $traverser->addVisitor($visitor);
  97. $traverser->addVisitor($visitor2);
  98. $stmts = [$str1Node, $str2Node];
  99. $this->assertEquals([$str2Node], $traverser->traverse($stmts));
  100. $this->assertEquals([
  101. ['beforeTraverse', $stmts],
  102. ['enterNode', $str2Node],
  103. ['leaveNode', $str2Node],
  104. ['afterTraverse', [$str2Node]],
  105. ], $visitor2->trace);
  106. }
  107. public function testReturnArrayFromEnter(): void {
  108. $str1Node = new String_('Str1');
  109. $str2Node = new String_('Str2');
  110. $str3Node = new String_('Str3');
  111. $str4Node = new String_('Str4');
  112. $visitor = new NodeVisitorForTesting([
  113. ['enterNode', $str1Node, [$str3Node, $str4Node]],
  114. ]);
  115. $visitor2 = new NodeVisitorForTesting();
  116. $traverser = new NodeTraverser();
  117. $traverser->addVisitor($visitor);
  118. $traverser->addVisitor($visitor2);
  119. $stmts = [$str1Node, $str2Node];
  120. $this->assertEquals([$str3Node, $str4Node, $str2Node], $traverser->traverse($stmts));
  121. $this->assertEquals([
  122. ['beforeTraverse', $stmts],
  123. ['enterNode', $str2Node],
  124. ['leaveNode', $str2Node],
  125. ['afterTraverse', [$str3Node, $str4Node, $str2Node]],
  126. ], $visitor2->trace);
  127. }
  128. public function testMerge(): void {
  129. $strStart = new String_('Start');
  130. $strMiddle = new String_('End');
  131. $strEnd = new String_('Middle');
  132. $strR1 = new String_('Replacement 1');
  133. $strR2 = new String_('Replacement 2');
  134. $visitor = new NodeVisitorForTesting([
  135. ['leaveNode', $strMiddle, [$strR1, $strR2]],
  136. ]);
  137. $traverser = new NodeTraverser();
  138. $traverser->addVisitor($visitor);
  139. $this->assertEquals(
  140. [$strStart, $strR1, $strR2, $strEnd],
  141. $traverser->traverse([$strStart, $strMiddle, $strEnd])
  142. );
  143. }
  144. public function testInvalidDeepArray(): void {
  145. $this->expectException(\LogicException::class);
  146. $this->expectExceptionMessage('Invalid node structure: Contains nested arrays');
  147. $strNode = new String_('Foo');
  148. $stmts = [[[$strNode]]];
  149. $traverser = new NodeTraverser();
  150. $this->assertEquals($stmts, $traverser->traverse($stmts));
  151. }
  152. public function testDontTraverseChildren(): void {
  153. $strNode = new String_('str');
  154. $printNode = new Expr\Print_($strNode);
  155. $varNode = new Expr\Variable('foo');
  156. $mulNode = new Expr\BinaryOp\Mul($varNode, $varNode);
  157. $negNode = new Expr\UnaryMinus($mulNode);
  158. $stmts = [$printNode, $negNode];
  159. $visitor1 = new NodeVisitorForTesting([
  160. ['enterNode', $printNode, NodeVisitor::DONT_TRAVERSE_CHILDREN],
  161. ]);
  162. $visitor2 = new NodeVisitorForTesting([
  163. ['enterNode', $mulNode, NodeVisitor::DONT_TRAVERSE_CHILDREN],
  164. ]);
  165. $expectedTrace = [
  166. ['beforeTraverse', $stmts],
  167. ['enterNode', $printNode],
  168. ['leaveNode', $printNode],
  169. ['enterNode', $negNode],
  170. ['enterNode', $mulNode],
  171. ['leaveNode', $mulNode],
  172. ['leaveNode', $negNode],
  173. ['afterTraverse', $stmts],
  174. ];
  175. $traverser = new NodeTraverser();
  176. $traverser->addVisitor($visitor1);
  177. $traverser->addVisitor($visitor2);
  178. $this->assertEquals($stmts, $traverser->traverse($stmts));
  179. $this->assertEquals($expectedTrace, $visitor1->trace);
  180. $this->assertEquals($expectedTrace, $visitor2->trace);
  181. }
  182. public function testDontTraverseCurrentAndChildren(): void {
  183. // print 'str'; -($foo * $foo);
  184. $strNode = new String_('str');
  185. $printNode = new Expr\Print_($strNode);
  186. $varNode = new Expr\Variable('foo');
  187. $mulNode = new Expr\BinaryOp\Mul($varNode, $varNode);
  188. $divNode = new Expr\BinaryOp\Div($varNode, $varNode);
  189. $negNode = new Expr\UnaryMinus($mulNode);
  190. $stmts = [$printNode, $negNode];
  191. $visitor1 = new NodeVisitorForTesting([
  192. ['enterNode', $printNode, NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN],
  193. ['enterNode', $mulNode, NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN],
  194. ['leaveNode', $mulNode, $divNode],
  195. ]);
  196. $visitor2 = new NodeVisitorForTesting();
  197. $traverser = new NodeTraverser();
  198. $traverser->addVisitor($visitor1);
  199. $traverser->addVisitor($visitor2);
  200. $resultStmts = $traverser->traverse($stmts);
  201. $this->assertInstanceOf(Expr\BinaryOp\Div::class, $resultStmts[1]->expr);
  202. $this->assertEquals([
  203. ['beforeTraverse', $stmts],
  204. ['enterNode', $printNode],
  205. ['leaveNode', $printNode],
  206. ['enterNode', $negNode],
  207. ['enterNode', $mulNode],
  208. ['leaveNode', $mulNode],
  209. ['leaveNode', $negNode],
  210. ['afterTraverse', $resultStmts],
  211. ], $visitor1->trace);
  212. $this->assertEquals([
  213. ['beforeTraverse', $stmts],
  214. ['enterNode', $negNode],
  215. ['leaveNode', $negNode],
  216. ['afterTraverse', $resultStmts],
  217. ], $visitor2->trace);
  218. }
  219. public function testStopTraversal(): void {
  220. $varNode1 = new Expr\Variable('a');
  221. $varNode2 = new Expr\Variable('b');
  222. $varNode3 = new Expr\Variable('c');
  223. $mulNode = new Expr\BinaryOp\Mul($varNode1, $varNode2);
  224. $printNode = new Expr\Print_($varNode3);
  225. $stmts = [$mulNode, $printNode];
  226. // From enterNode() with array parent
  227. $visitor = new NodeVisitorForTesting([
  228. ['enterNode', $mulNode, NodeVisitor::STOP_TRAVERSAL],
  229. ]);
  230. $traverser = new NodeTraverser();
  231. $traverser->addVisitor($visitor);
  232. $this->assertEquals($stmts, $traverser->traverse($stmts));
  233. $this->assertEquals([
  234. ['beforeTraverse', $stmts],
  235. ['enterNode', $mulNode],
  236. ['afterTraverse', $stmts],
  237. ], $visitor->trace);
  238. // From enterNode with Node parent
  239. $visitor = new NodeVisitorForTesting([
  240. ['enterNode', $varNode1, NodeVisitor::STOP_TRAVERSAL],
  241. ]);
  242. $traverser = new NodeTraverser();
  243. $traverser->addVisitor($visitor);
  244. $this->assertEquals($stmts, $traverser->traverse($stmts));
  245. $this->assertEquals([
  246. ['beforeTraverse', $stmts],
  247. ['enterNode', $mulNode],
  248. ['enterNode', $varNode1],
  249. ['afterTraverse', $stmts],
  250. ], $visitor->trace);
  251. // From leaveNode with Node parent
  252. $visitor = new NodeVisitorForTesting([
  253. ['leaveNode', $varNode1, NodeVisitor::STOP_TRAVERSAL],
  254. ]);
  255. $traverser = new NodeTraverser();
  256. $traverser->addVisitor($visitor);
  257. $this->assertEquals($stmts, $traverser->traverse($stmts));
  258. $this->assertEquals([
  259. ['beforeTraverse', $stmts],
  260. ['enterNode', $mulNode],
  261. ['enterNode', $varNode1],
  262. ['leaveNode', $varNode1],
  263. ['afterTraverse', $stmts],
  264. ], $visitor->trace);
  265. // From leaveNode with array parent
  266. $visitor = new NodeVisitorForTesting([
  267. ['leaveNode', $mulNode, NodeVisitor::STOP_TRAVERSAL],
  268. ]);
  269. $traverser = new NodeTraverser();
  270. $traverser->addVisitor($visitor);
  271. $this->assertEquals($stmts, $traverser->traverse($stmts));
  272. $this->assertEquals([
  273. ['beforeTraverse', $stmts],
  274. ['enterNode', $mulNode],
  275. ['enterNode', $varNode1],
  276. ['leaveNode', $varNode1],
  277. ['enterNode', $varNode2],
  278. ['leaveNode', $varNode2],
  279. ['leaveNode', $mulNode],
  280. ['afterTraverse', $stmts],
  281. ], $visitor->trace);
  282. // Check that pending array modifications are still carried out
  283. $visitor = new NodeVisitorForTesting([
  284. ['leaveNode', $mulNode, NodeVisitor::REMOVE_NODE],
  285. ['enterNode', $printNode, NodeVisitor::STOP_TRAVERSAL],
  286. ]);
  287. $traverser = new NodeTraverser();
  288. $traverser->addVisitor($visitor);
  289. $this->assertEquals([$printNode], $traverser->traverse($stmts));
  290. $this->assertEquals([
  291. ['beforeTraverse', $stmts],
  292. ['enterNode', $mulNode],
  293. ['enterNode', $varNode1],
  294. ['leaveNode', $varNode1],
  295. ['enterNode', $varNode2],
  296. ['leaveNode', $varNode2],
  297. ['leaveNode', $mulNode],
  298. ['enterNode', $printNode],
  299. ['afterTraverse', [$printNode]],
  300. ], $visitor->trace);
  301. }
  302. public function testReplaceWithNull(): void {
  303. $one = new Int_(1);
  304. $else1 = new Else_();
  305. $else2 = new Else_();
  306. $if1 = new If_($one, ['else' => $else1]);
  307. $if2 = new If_($one, ['else' => $else2]);
  308. $stmts = [$if1, $if2];
  309. $visitor1 = new NodeVisitorForTesting([
  310. ['enterNode', $else1, NodeVisitor::REPLACE_WITH_NULL],
  311. ['leaveNode', $else2, NodeVisitor::REPLACE_WITH_NULL],
  312. ]);
  313. $visitor2 = new NodeVisitorForTesting();
  314. $traverser = new NodeTraverser();
  315. $traverser->addVisitor($visitor1);
  316. $traverser->addVisitor($visitor2);
  317. $newStmts = $traverser->traverse($stmts);
  318. $this->assertEquals([
  319. new If_($one),
  320. new If_($one),
  321. ], $newStmts);
  322. $this->assertEquals([
  323. ['beforeTraverse', $stmts],
  324. ['enterNode', $if1],
  325. ['enterNode', $one],
  326. // We never see the if1 Else node.
  327. ['leaveNode', $one],
  328. ['leaveNode', $if1],
  329. ['enterNode', $if2],
  330. ['enterNode', $one],
  331. ['leaveNode', $one],
  332. // We do see the if2 Else node, as it will only be replaced afterwards.
  333. ['enterNode', $else2],
  334. ['leaveNode', $else2],
  335. ['leaveNode', $if2],
  336. ['afterTraverse', $stmts],
  337. ], $visitor2->trace);
  338. }
  339. public function testRemovingVisitor(): void {
  340. $visitor1 = new class () extends NodeVisitorAbstract {};
  341. $visitor2 = new class () extends NodeVisitorAbstract {};
  342. $visitor3 = new class () extends NodeVisitorAbstract {};
  343. $traverser = new NodeTraverser();
  344. $traverser->addVisitor($visitor1);
  345. $traverser->addVisitor($visitor2);
  346. $traverser->addVisitor($visitor3);
  347. $getVisitors = (function () {
  348. return $this->visitors;
  349. })->bindTo($traverser, NodeTraverser::class);
  350. $preExpected = [$visitor1, $visitor2, $visitor3];
  351. $this->assertSame($preExpected, $getVisitors());
  352. $traverser->removeVisitor($visitor2);
  353. $postExpected = [$visitor1, $visitor3];
  354. $this->assertSame($postExpected, $getVisitors());
  355. }
  356. public function testNoCloneNodes(): void {
  357. $stmts = [new Node\Stmt\Echo_([new String_('Foo'), new String_('Bar')])];
  358. $traverser = new NodeTraverser();
  359. $this->assertSame($stmts, $traverser->traverse($stmts));
  360. }
  361. /**
  362. * @dataProvider provideTestInvalidReturn
  363. */
  364. public function testInvalidReturn($stmts, $visitor, $message): void {
  365. $this->expectException(\LogicException::class);
  366. $this->expectExceptionMessage($message);
  367. $traverser = new NodeTraverser();
  368. $traverser->addVisitor($visitor);
  369. $traverser->traverse($stmts);
  370. }
  371. public static function provideTestInvalidReturn() {
  372. $num = new Node\Scalar\Int_(42);
  373. $expr = new Node\Stmt\Expression($num);
  374. $stmts = [$expr];
  375. $visitor1 = new NodeVisitorForTesting([
  376. ['enterNode', $expr, 'foobar'],
  377. ]);
  378. $visitor2 = new NodeVisitorForTesting([
  379. ['enterNode', $num, 'foobar'],
  380. ]);
  381. $visitor3 = new NodeVisitorForTesting([
  382. ['leaveNode', $num, 'foobar'],
  383. ]);
  384. $visitor4 = new NodeVisitorForTesting([
  385. ['leaveNode', $expr, 'foobar'],
  386. ]);
  387. $visitor5 = new NodeVisitorForTesting([
  388. ['leaveNode', $num, [new Node\Scalar\Float_(42.0)]],
  389. ]);
  390. $visitor6 = new NodeVisitorForTesting([
  391. ['leaveNode', $expr, false],
  392. ]);
  393. $visitor7 = new NodeVisitorForTesting([
  394. ['enterNode', $expr, new Node\Scalar\Int_(42)],
  395. ]);
  396. $visitor8 = new NodeVisitorForTesting([
  397. ['enterNode', $num, new Node\Stmt\Return_()],
  398. ]);
  399. $visitor9 = new NodeVisitorForTesting([
  400. ['enterNode', $expr, NodeVisitor::REPLACE_WITH_NULL],
  401. ]);
  402. $visitor10 = new NodeVisitorForTesting([
  403. ['leaveNode', $expr, NodeVisitor::REPLACE_WITH_NULL],
  404. ]);
  405. return [
  406. [$stmts, $visitor1, 'enterNode() returned invalid value of type string'],
  407. [$stmts, $visitor2, 'enterNode() returned invalid value of type string'],
  408. [$stmts, $visitor3, 'leaveNode() returned invalid value of type string'],
  409. [$stmts, $visitor4, 'leaveNode() returned invalid value of type string'],
  410. [$stmts, $visitor5, 'leaveNode() may only return an array if the parent structure is an array'],
  411. [$stmts, $visitor6, 'leaveNode() returned invalid value of type bool'],
  412. [$stmts, $visitor7, 'Trying to replace statement (Stmt_Expression) with expression (Scalar_Int). Are you missing a Stmt_Expression wrapper?'],
  413. [$stmts, $visitor8, 'Trying to replace expression (Scalar_Int) with statement (Stmt_Return)'],
  414. [$stmts, $visitor9, 'REPLACE_WITH_NULL can not be used if the parent structure is an array'],
  415. [$stmts, $visitor10, 'REPLACE_WITH_NULL can not be used if the parent structure is an array'],
  416. ];
  417. }
  418. }