1<?php declare(strict_types=1); 2 3namespace PhpParser; 4 5class DummyNode extends NodeAbstract { 6 public $subNode1; 7 public $subNode2; 8 public $notSubNode; 9 10 public function __construct($subNode1, $subNode2, $notSubNode, $attributes) { 11 parent::__construct($attributes); 12 $this->subNode1 = $subNode1; 13 $this->subNode2 = $subNode2; 14 $this->notSubNode = $notSubNode; 15 } 16 17 public function getSubNodeNames(): array { 18 return ['subNode1', 'subNode2']; 19 } 20 21 // This method is only overwritten because the node is located in an unusual namespace 22 public function getType(): string { 23 return 'Dummy'; 24 } 25} 26 27class NodeAbstractTest extends \PHPUnit\Framework\TestCase { 28 public static function provideNodes() { 29 $attributes = [ 30 'startLine' => 10, 31 'endLine' => 11, 32 'startTokenPos' => 12, 33 'endTokenPos' => 13, 34 'startFilePos' => 14, 35 'endFilePos' => 15, 36 'comments' => [ 37 new Comment('// Comment 1' . "\n"), 38 new Comment\Doc('/** doc comment */'), 39 new Comment('// Comment 2' . "\n"), 40 ], 41 ]; 42 43 $node = new DummyNode('value1', 'value2', 'value3', $attributes); 44 45 return [ 46 [$attributes, $node], 47 ]; 48 } 49 50 /** 51 * @dataProvider provideNodes 52 */ 53 public function testConstruct(array $attributes, Node $node) { 54 $this->assertSame('Dummy', $node->getType()); 55 $this->assertSame(['subNode1', 'subNode2'], $node->getSubNodeNames()); 56 $this->assertSame(10, $node->getLine()); 57 $this->assertSame(10, $node->getStartLine()); 58 $this->assertSame(11, $node->getEndLine()); 59 $this->assertSame(12, $node->getStartTokenPos()); 60 $this->assertSame(13, $node->getEndTokenPos()); 61 $this->assertSame(14, $node->getStartFilePos()); 62 $this->assertSame(15, $node->getEndFilePos()); 63 $this->assertSame('/** doc comment */', $node->getDocComment()->getText()); 64 $this->assertSame('value1', $node->subNode1); 65 $this->assertSame('value2', $node->subNode2); 66 $this->assertTrue(isset($node->subNode1)); 67 $this->assertTrue(isset($node->subNode2)); 68 $this->assertTrue(!isset($node->subNode3)); 69 $this->assertSame($attributes, $node->getAttributes()); 70 $this->assertSame($attributes['comments'], $node->getComments()); 71 72 return $node; 73 } 74 75 /** 76 * @dataProvider provideNodes 77 */ 78 public function testGetDocComment(array $attributes, Node $node): void { 79 $this->assertSame('/** doc comment */', $node->getDocComment()->getText()); 80 $comments = $node->getComments(); 81 82 array_splice($comments, 1, 1, []); // remove doc comment 83 $node->setAttribute('comments', $comments); 84 $this->assertNull($node->getDocComment()); 85 86 // Remove all comments. 87 $node->setAttribute('comments', []); 88 $this->assertNull($node->getDocComment()); 89 } 90 91 public function testSetDocComment(): void { 92 $node = new DummyNode(null, null, null, []); 93 94 // Add doc comment to node without comments 95 $docComment = new Comment\Doc('/** doc */'); 96 $node->setDocComment($docComment); 97 $this->assertSame($docComment, $node->getDocComment()); 98 99 // Replace it 100 $docComment = new Comment\Doc('/** doc 2 */'); 101 $node->setDocComment($docComment); 102 $this->assertSame($docComment, $node->getDocComment()); 103 104 // Add docmment to node with other comments 105 $c1 = new Comment('/* foo */'); 106 $c2 = new Comment('/* bar */'); 107 $docComment = new Comment\Doc('/** baz */'); 108 $node->setAttribute('comments', [$c1, $c2]); 109 $node->setDocComment($docComment); 110 $this->assertSame([$c1, $c2, $docComment], $node->getAttribute('comments')); 111 112 // Replace doc comment that is not at the end. 113 $newDocComment = new Comment\Doc('/** new baz */'); 114 $node->setAttribute('comments', [$c1, $docComment, $c2]); 115 $node->setDocComment($newDocComment); 116 $this->assertSame([$c1, $newDocComment, $c2], $node->getAttribute('comments')); 117 } 118 119 /** 120 * @dataProvider provideNodes 121 */ 122 public function testChange(array $attributes, DummyNode $node): void { 123 // direct modification 124 $node->subNode1 = 'newValue'; 125 $this->assertSame('newValue', $node->subNode1); 126 127 // indirect modification 128 $subNode = &$node->subNode1; 129 $subNode = 'newNewValue'; 130 $this->assertSame('newNewValue', $node->subNode1); 131 132 // removal 133 unset($node->subNode1); 134 $this->assertFalse(isset($node->subNode1)); 135 } 136 137 /** 138 * @dataProvider provideNodes 139 */ 140 public function testIteration(array $attributes, Node $node): void { 141 // Iteration is simple object iteration over properties, 142 // not over subnodes 143 $i = 0; 144 foreach ($node as $key => $value) { 145 if ($i === 0) { 146 $this->assertSame('subNode1', $key); 147 $this->assertSame('value1', $value); 148 } elseif ($i === 1) { 149 $this->assertSame('subNode2', $key); 150 $this->assertSame('value2', $value); 151 } elseif ($i === 2) { 152 $this->assertSame('notSubNode', $key); 153 $this->assertSame('value3', $value); 154 } else { 155 throw new \Exception(); 156 } 157 $i++; 158 } 159 $this->assertSame(3, $i); 160 } 161 162 public function testAttributes(): void { 163 /** @var $node Node */ 164 $node = $this->getMockForAbstractClass(NodeAbstract::class); 165 166 $this->assertEmpty($node->getAttributes()); 167 168 $node->setAttribute('key', 'value'); 169 $this->assertTrue($node->hasAttribute('key')); 170 $this->assertSame('value', $node->getAttribute('key')); 171 172 $this->assertFalse($node->hasAttribute('doesNotExist')); 173 $this->assertNull($node->getAttribute('doesNotExist')); 174 $this->assertSame('default', $node->getAttribute('doesNotExist', 'default')); 175 176 $node->setAttribute('null', null); 177 $this->assertTrue($node->hasAttribute('null')); 178 $this->assertNull($node->getAttribute('null')); 179 $this->assertNull($node->getAttribute('null', 'default')); 180 181 $this->assertSame( 182 [ 183 'key' => 'value', 184 'null' => null, 185 ], 186 $node->getAttributes() 187 ); 188 189 $node->setAttributes( 190 [ 191 'a' => 'b', 192 'c' => null, 193 ] 194 ); 195 $this->assertSame( 196 [ 197 'a' => 'b', 198 'c' => null, 199 ], 200 $node->getAttributes() 201 ); 202 } 203 204 public function testJsonSerialization(): void { 205 $code = <<<'PHP' 206<?php 207// comment 208/** doc comment */ 209function functionName(&$a = 0, $b = 1.0) { 210 echo 'Foo'; 211} 212PHP; 213 $expected = <<<'JSON' 214[ 215 { 216 "nodeType": "Stmt_Function", 217 "byRef": false, 218 "name": { 219 "nodeType": "Identifier", 220 "name": "functionName", 221 "attributes": { 222 "startLine": 4, 223 "startTokenPos": 7, 224 "startFilePos": 45, 225 "endLine": 4, 226 "endTokenPos": 7, 227 "endFilePos": 56 228 } 229 }, 230 "params": [ 231 { 232 "nodeType": "Param", 233 "type": null, 234 "byRef": true, 235 "variadic": false, 236 "var": { 237 "nodeType": "Expr_Variable", 238 "name": "a", 239 "attributes": { 240 "startLine": 4, 241 "startTokenPos": 10, 242 "startFilePos": 59, 243 "endLine": 4, 244 "endTokenPos": 10, 245 "endFilePos": 60 246 } 247 }, 248 "default": { 249 "nodeType": "Scalar_Int", 250 "value": 0, 251 "attributes": { 252 "startLine": 4, 253 "startTokenPos": 14, 254 "startFilePos": 64, 255 "endLine": 4, 256 "endTokenPos": 14, 257 "endFilePos": 64, 258 "rawValue": "0", 259 "kind": 10 260 } 261 }, 262 "flags": 0, 263 "attrGroups": [], 264 "hooks": [], 265 "attributes": { 266 "startLine": 4, 267 "startTokenPos": 9, 268 "startFilePos": 58, 269 "endLine": 4, 270 "endTokenPos": 14, 271 "endFilePos": 64 272 } 273 }, 274 { 275 "nodeType": "Param", 276 "type": null, 277 "byRef": false, 278 "variadic": false, 279 "var": { 280 "nodeType": "Expr_Variable", 281 "name": "b", 282 "attributes": { 283 "startLine": 4, 284 "startTokenPos": 17, 285 "startFilePos": 67, 286 "endLine": 4, 287 "endTokenPos": 17, 288 "endFilePos": 68 289 } 290 }, 291 "default": { 292 "nodeType": "Scalar_Float", 293 "value": 1, 294 "attributes": { 295 "startLine": 4, 296 "startTokenPos": 21, 297 "startFilePos": 72, 298 "endLine": 4, 299 "endTokenPos": 21, 300 "endFilePos": 74, 301 "rawValue": "1.0" 302 } 303 }, 304 "flags": 0, 305 "attrGroups": [], 306 "hooks": [], 307 "attributes": { 308 "startLine": 4, 309 "startTokenPos": 17, 310 "startFilePos": 67, 311 "endLine": 4, 312 "endTokenPos": 21, 313 "endFilePos": 74 314 } 315 } 316 ], 317 "returnType": null, 318 "stmts": [ 319 { 320 "nodeType": "Stmt_Echo", 321 "exprs": [ 322 { 323 "nodeType": "Scalar_String", 324 "value": "Foo", 325 "attributes": { 326 "startLine": 5, 327 "startTokenPos": 28, 328 "startFilePos": 88, 329 "endLine": 5, 330 "endTokenPos": 28, 331 "endFilePos": 92, 332 "kind": 1, 333 "rawValue": "'Foo'" 334 } 335 } 336 ], 337 "attributes": { 338 "startLine": 5, 339 "startTokenPos": 26, 340 "startFilePos": 83, 341 "endLine": 5, 342 "endTokenPos": 29, 343 "endFilePos": 93 344 } 345 } 346 ], 347 "attrGroups": [], 348 "attributes": { 349 "startLine": 4, 350 "startTokenPos": 5, 351 "startFilePos": 36, 352 "endLine": 6, 353 "endTokenPos": 31, 354 "endFilePos": 95, 355 "comments": [ 356 { 357 "nodeType": "Comment", 358 "text": "\/\/ comment", 359 "line": 2, 360 "filePos": 6, 361 "tokenPos": 1, 362 "endLine": 2, 363 "endFilePos": 15, 364 "endTokenPos": 1 365 }, 366 { 367 "nodeType": "Comment_Doc", 368 "text": "\/** doc comment *\/", 369 "line": 3, 370 "filePos": 17, 371 "tokenPos": 3, 372 "endLine": 3, 373 "endFilePos": 34, 374 "endTokenPos": 3 375 } 376 ] 377 } 378 } 379] 380JSON; 381 $expected81 = <<<'JSON' 382[ 383 { 384 "nodeType": "Stmt_Function", 385 "attributes": { 386 "startLine": 4, 387 "startTokenPos": 5, 388 "startFilePos": 36, 389 "endLine": 6, 390 "endTokenPos": 31, 391 "endFilePos": 95, 392 "comments": [ 393 { 394 "nodeType": "Comment", 395 "text": "\/\/ comment", 396 "line": 2, 397 "filePos": 6, 398 "tokenPos": 1, 399 "endLine": 2, 400 "endFilePos": 15, 401 "endTokenPos": 1 402 }, 403 { 404 "nodeType": "Comment_Doc", 405 "text": "\/** doc comment *\/", 406 "line": 3, 407 "filePos": 17, 408 "tokenPos": 3, 409 "endLine": 3, 410 "endFilePos": 34, 411 "endTokenPos": 3 412 } 413 ] 414 }, 415 "byRef": false, 416 "name": { 417 "nodeType": "Identifier", 418 "attributes": { 419 "startLine": 4, 420 "startTokenPos": 7, 421 "startFilePos": 45, 422 "endLine": 4, 423 "endTokenPos": 7, 424 "endFilePos": 56 425 }, 426 "name": "functionName" 427 }, 428 "params": [ 429 { 430 "nodeType": "Param", 431 "attributes": { 432 "startLine": 4, 433 "startTokenPos": 9, 434 "startFilePos": 58, 435 "endLine": 4, 436 "endTokenPos": 14, 437 "endFilePos": 64 438 }, 439 "type": null, 440 "byRef": true, 441 "variadic": false, 442 "var": { 443 "nodeType": "Expr_Variable", 444 "attributes": { 445 "startLine": 4, 446 "startTokenPos": 10, 447 "startFilePos": 59, 448 "endLine": 4, 449 "endTokenPos": 10, 450 "endFilePos": 60 451 }, 452 "name": "a" 453 }, 454 "default": { 455 "nodeType": "Scalar_Int", 456 "attributes": { 457 "startLine": 4, 458 "startTokenPos": 14, 459 "startFilePos": 64, 460 "endLine": 4, 461 "endTokenPos": 14, 462 "endFilePos": 64, 463 "rawValue": "0", 464 "kind": 10 465 }, 466 "value": 0 467 }, 468 "flags": 0, 469 "attrGroups": [], 470 "hooks": [] 471 }, 472 { 473 "nodeType": "Param", 474 "attributes": { 475 "startLine": 4, 476 "startTokenPos": 17, 477 "startFilePos": 67, 478 "endLine": 4, 479 "endTokenPos": 21, 480 "endFilePos": 74 481 }, 482 "type": null, 483 "byRef": false, 484 "variadic": false, 485 "var": { 486 "nodeType": "Expr_Variable", 487 "attributes": { 488 "startLine": 4, 489 "startTokenPos": 17, 490 "startFilePos": 67, 491 "endLine": 4, 492 "endTokenPos": 17, 493 "endFilePos": 68 494 }, 495 "name": "b" 496 }, 497 "default": { 498 "nodeType": "Scalar_Float", 499 "attributes": { 500 "startLine": 4, 501 "startTokenPos": 21, 502 "startFilePos": 72, 503 "endLine": 4, 504 "endTokenPos": 21, 505 "endFilePos": 74, 506 "rawValue": "1.0" 507 }, 508 "value": 1 509 }, 510 "flags": 0, 511 "attrGroups": [], 512 "hooks": [] 513 } 514 ], 515 "returnType": null, 516 "stmts": [ 517 { 518 "nodeType": "Stmt_Echo", 519 "attributes": { 520 "startLine": 5, 521 "startTokenPos": 26, 522 "startFilePos": 83, 523 "endLine": 5, 524 "endTokenPos": 29, 525 "endFilePos": 93 526 }, 527 "exprs": [ 528 { 529 "nodeType": "Scalar_String", 530 "attributes": { 531 "startLine": 5, 532 "startTokenPos": 28, 533 "startFilePos": 88, 534 "endLine": 5, 535 "endTokenPos": 28, 536 "endFilePos": 92, 537 "kind": 1, 538 "rawValue": "'Foo'" 539 }, 540 "value": "Foo" 541 } 542 ] 543 } 544 ], 545 "attrGroups": [] 546 } 547] 548JSON; 549 550 if (version_compare(PHP_VERSION, '8.1', '>=')) { 551 $expected = $expected81; 552 } 553 554 $parser = new Parser\Php7(new Lexer()); 555 $stmts = $parser->parse(canonicalize($code)); 556 $json = json_encode($stmts, JSON_PRETTY_PRINT); 557 $this->assertEquals(canonicalize($expected), canonicalize($json)); 558 } 559} 560