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