1<?php declare(strict_types=1); 2 3namespace PhpParser\NodeVisitor; 4 5use PhpParser; 6use PhpParser\Node; 7use PhpParser\Node\Expr; 8use PhpParser\Node\Name; 9use PhpParser\Node\Stmt; 10 11class NameResolverTest extends \PHPUnit\Framework\TestCase { 12 private function canonicalize($string) { 13 return str_replace("\r\n", "\n", $string); 14 } 15 16 /** 17 * @covers \PhpParser\NodeVisitor\NameResolver 18 */ 19 public function testResolveNames(): void { 20 $code = <<<'EOC' 21<?php 22 23namespace Foo { 24 use Hallo as Hi; 25 26 new Bar(); 27 new Hi(); 28 new Hi\Bar(); 29 new \Bar(); 30 new namespace\Bar(); 31 32 bar(); 33 hi(); 34 Hi\bar(); 35 foo\bar(); 36 \bar(); 37 namespace\bar(); 38} 39namespace { 40 use Hallo as Hi; 41 42 new Bar(); 43 new Hi(); 44 new Hi\Bar(); 45 new \Bar(); 46 new namespace\Bar(); 47 48 bar(); 49 hi(); 50 Hi\bar(); 51 foo\bar(); 52 \bar(); 53 namespace\bar(); 54} 55namespace Bar { 56 use function foo\bar as baz; 57 use const foo\BAR as BAZ; 58 use foo as bar; 59 60 bar(); 61 baz(); 62 bar\foo(); 63 baz\foo(); 64 BAR(); 65 BAZ(); 66 BAR\FOO(); 67 BAZ\FOO(); 68 69 bar; 70 baz; 71 bar\foo; 72 baz\foo; 73 BAR; 74 BAZ; 75 BAR\FOO; 76 BAZ\FOO; 77} 78namespace Baz { 79 use A\T\{B\C, D\E}; 80 use function X\T\{b\c, d\e}; 81 use const Y\T\{B\C, D\E}; 82 use Z\T\{G, function f, const K}; 83 84 new C; 85 new E; 86 new C\D; 87 new E\F; 88 new G; 89 90 c(); 91 e(); 92 f(); 93 C; 94 E; 95 K; 96} 97EOC; 98 $expectedCode = <<<'EOC' 99namespace Foo { 100 use Hallo as Hi; 101 new \Foo\Bar(); 102 new \Hallo(); 103 new \Hallo\Bar(); 104 new \Bar(); 105 new \Foo\Bar(); 106 bar(); 107 hi(); 108 \Hallo\bar(); 109 \Foo\foo\bar(); 110 \bar(); 111 \Foo\bar(); 112} 113namespace { 114 use Hallo as Hi; 115 new \Bar(); 116 new \Hallo(); 117 new \Hallo\Bar(); 118 new \Bar(); 119 new \Bar(); 120 \bar(); 121 \hi(); 122 \Hallo\bar(); 123 \foo\bar(); 124 \bar(); 125 \bar(); 126} 127namespace Bar { 128 use function foo\bar as baz; 129 use const foo\BAR as BAZ; 130 use foo as bar; 131 bar(); 132 \foo\bar(); 133 \foo\foo(); 134 \Bar\baz\foo(); 135 BAR(); 136 \foo\bar(); 137 \foo\FOO(); 138 \Bar\BAZ\FOO(); 139 bar; 140 baz; 141 \foo\foo; 142 \Bar\baz\foo; 143 BAR; 144 \foo\BAR; 145 \foo\FOO; 146 \Bar\BAZ\FOO; 147} 148namespace Baz { 149 use A\T\{B\C, D\E}; 150 use function X\T\{b\c, d\e}; 151 use const Y\T\{B\C, D\E}; 152 use Z\T\{G, function f, const K}; 153 new \A\T\B\C(); 154 new \A\T\D\E(); 155 new \A\T\B\C\D(); 156 new \A\T\D\E\F(); 157 new \Z\T\G(); 158 \X\T\b\c(); 159 \X\T\d\e(); 160 \Z\T\f(); 161 \Y\T\B\C; 162 \Y\T\D\E; 163 \Z\T\K; 164} 165EOC; 166 167 $prettyPrinter = new PhpParser\PrettyPrinter\Standard(); 168 $stmts = $this->parseAndResolve($code); 169 170 $this->assertSame( 171 $this->canonicalize($expectedCode), 172 $prettyPrinter->prettyPrint($stmts) 173 ); 174 } 175 176 /** 177 * @covers \PhpParser\NodeVisitor\NameResolver 178 */ 179 public function testResolveLocations(): void { 180 $code = <<<'EOC' 181<?php 182namespace NS; 183 184#[X] 185class A extends B implements C, D { 186 use E, F, G { 187 f as private g; 188 E::h as i; 189 E::j insteadof F, G; 190 } 191 192 #[X] 193 public float $php = 7.4; 194 public ?Foo $person; 195 protected static ?bool $probability; 196 public A|B|int $prop; 197 198 #[X] 199 const C = 1; 200 201 public const X A = X::Bar; 202 public const X\Foo B = X\Foo::Bar; 203 public const \X\Foo C = \X\Foo::Bar; 204 205 public Foo $foo { 206 #[X] 207 set(#[X] Bar $v) {} 208 } 209 210 public function __construct( 211 public Foo $bar { 212 #[X] 213 set(#[X] Bar $v) {} 214 } 215 ) {} 216} 217 218#[X] 219interface A extends C, D { 220 public function a(A $a) : A; 221 public function b(A|B|int $a): A|B|int; 222 public function c(A&B $a): A&B; 223} 224 225#[X] 226enum E: int { 227 #[X] 228 case A = 1; 229} 230 231#[X] 232trait A {} 233 234#[X] 235function f(#[X] A $a) : A {} 236function f2(array $a) : array {} 237function fn3(?A $a) : ?A {} 238function fn4(?array $a) : ?array {} 239 240#[X] 241function(A $a) : A {}; 242 243#[X] 244fn(array $a): array => $a; 245fn(A $a): A => $a; 246fn(?A $a): ?A => $a; 247 248A::b(); 249A::$b; 250A::B; 251new A; 252$a instanceof A; 253 254namespace\a(); 255namespace\A; 256 257try { 258 $someThing; 259} catch (A $a) { 260 $someThingElse; 261} 262EOC; 263 $expectedCode = <<<'EOC' 264namespace NS; 265 266#[\NS\X] 267class A extends \NS\B implements \NS\C, \NS\D 268{ 269 use \NS\E, \NS\F, \NS\G { 270 f as private g; 271 \NS\E::h as i; 272 \NS\E::j insteadof \NS\F, \NS\G; 273 } 274 #[\NS\X] 275 public float $php = 7.4; 276 public ?\NS\Foo $person; 277 protected static ?bool $probability; 278 public \NS\A|\NS\B|int $prop; 279 #[\NS\X] 280 const C = 1; 281 public const \NS\X A = \NS\X::Bar; 282 public const \NS\X\Foo B = \NS\X\Foo::Bar; 283 public const \X\Foo C = \X\Foo::Bar; 284 public \NS\Foo $foo { 285 #[\NS\X] 286 set(#[\NS\X] \NS\Bar $v) { 287 } 288 } 289 public function __construct(public \NS\Foo $bar { 290 #[\NS\X] 291 set(#[\NS\X] \NS\Bar $v) { 292 } 293 }) 294 { 295 } 296} 297#[\NS\X] 298interface A extends \NS\C, \NS\D 299{ 300 public function a(\NS\A $a): \NS\A; 301 public function b(\NS\A|\NS\B|int $a): \NS\A|\NS\B|int; 302 public function c(\NS\A&\NS\B $a): \NS\A&\NS\B; 303} 304#[\NS\X] 305enum E : int 306{ 307 #[\NS\X] 308 case A = 1; 309} 310#[\NS\X] 311trait A 312{ 313} 314#[\NS\X] 315function f(#[\NS\X] \NS\A $a): \NS\A 316{ 317} 318function f2(array $a): array 319{ 320} 321function fn3(?\NS\A $a): ?\NS\A 322{ 323} 324function fn4(?array $a): ?array 325{ 326} 327#[\NS\X] function (\NS\A $a): \NS\A { 328}; 329#[\NS\X] fn(array $a): array => $a; 330fn(\NS\A $a): \NS\A => $a; 331fn(?\NS\A $a): ?\NS\A => $a; 332\NS\A::b(); 333\NS\A::$b; 334\NS\A::B; 335new \NS\A(); 336$a instanceof \NS\A; 337\NS\a(); 338\NS\A; 339try { 340 $someThing; 341} catch (\NS\A $a) { 342 $someThingElse; 343} 344EOC; 345 346 $prettyPrinter = new PhpParser\PrettyPrinter\Standard(); 347 $stmts = $this->parseAndResolve($code); 348 349 $this->assertSame( 350 $this->canonicalize($expectedCode), 351 $prettyPrinter->prettyPrint($stmts) 352 ); 353 } 354 355 public function testNoResolveSpecialName(): void { 356 $stmts = [new Node\Expr\New_(new Name('self'))]; 357 358 $traverser = new PhpParser\NodeTraverser(); 359 $traverser->addVisitor(new NameResolver()); 360 361 $this->assertEquals($stmts, $traverser->traverse($stmts)); 362 } 363 364 public function testAddDeclarationNamespacedName(): void { 365 $nsStmts = [ 366 new Stmt\Class_('A'), 367 new Stmt\Interface_('B'), 368 new Stmt\Function_('C'), 369 new Stmt\Const_([ 370 new Node\Const_('D', new Node\Scalar\Int_(42)) 371 ]), 372 new Stmt\Trait_('E'), 373 new Expr\New_(new Stmt\Class_(null)), 374 new Stmt\Enum_('F'), 375 ]; 376 377 $traverser = new PhpParser\NodeTraverser(); 378 $traverser->addVisitor(new NameResolver()); 379 380 $stmts = $traverser->traverse([new Stmt\Namespace_(new Name('NS'), $nsStmts)]); 381 $this->assertSame('NS\\A', (string) $stmts[0]->stmts[0]->namespacedName); 382 $this->assertSame('NS\\B', (string) $stmts[0]->stmts[1]->namespacedName); 383 $this->assertSame('NS\\C', (string) $stmts[0]->stmts[2]->namespacedName); 384 $this->assertSame('NS\\D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName); 385 $this->assertSame('NS\\E', (string) $stmts[0]->stmts[4]->namespacedName); 386 $this->assertNull($stmts[0]->stmts[5]->class->namespacedName); 387 $this->assertSame('NS\\F', (string) $stmts[0]->stmts[6]->namespacedName); 388 389 $stmts = $traverser->traverse([new Stmt\Namespace_(null, $nsStmts)]); 390 $this->assertSame('A', (string) $stmts[0]->stmts[0]->namespacedName); 391 $this->assertSame('B', (string) $stmts[0]->stmts[1]->namespacedName); 392 $this->assertSame('C', (string) $stmts[0]->stmts[2]->namespacedName); 393 $this->assertSame('D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName); 394 $this->assertSame('E', (string) $stmts[0]->stmts[4]->namespacedName); 395 $this->assertNull($stmts[0]->stmts[5]->class->namespacedName); 396 $this->assertSame('F', (string) $stmts[0]->stmts[6]->namespacedName); 397 } 398 399 public function testAddRuntimeResolvedNamespacedName(): void { 400 $stmts = [ 401 new Stmt\Namespace_(new Name('NS'), [ 402 new Expr\FuncCall(new Name('foo')), 403 new Expr\ConstFetch(new Name('FOO')), 404 ]), 405 new Stmt\Namespace_(null, [ 406 new Expr\FuncCall(new Name('foo')), 407 new Expr\ConstFetch(new Name('FOO')), 408 ]), 409 ]; 410 411 $traverser = new PhpParser\NodeTraverser(); 412 $traverser->addVisitor(new NameResolver()); 413 $stmts = $traverser->traverse($stmts); 414 415 $this->assertSame('NS\\foo', (string) $stmts[0]->stmts[0]->name->getAttribute('namespacedName')); 416 $this->assertSame('NS\\FOO', (string) $stmts[0]->stmts[1]->name->getAttribute('namespacedName')); 417 418 $this->assertFalse($stmts[1]->stmts[0]->name->hasAttribute('namespacedName')); 419 $this->assertFalse($stmts[1]->stmts[1]->name->hasAttribute('namespacedName')); 420 } 421 422 /** 423 * @dataProvider provideTestError 424 */ 425 public function testError(Node $stmt, $errorMsg): void { 426 $this->expectException(\PhpParser\Error::class); 427 $this->expectExceptionMessage($errorMsg); 428 429 $traverser = new PhpParser\NodeTraverser(); 430 $traverser->addVisitor(new NameResolver()); 431 $traverser->traverse([$stmt]); 432 } 433 434 public static function provideTestError() { 435 return [ 436 [ 437 new Stmt\Use_([ 438 new Node\UseItem(new Name('A\B'), 'B', 0, ['startLine' => 1]), 439 new Node\UseItem(new Name('C\D'), 'B', 0, ['startLine' => 2]), 440 ], Stmt\Use_::TYPE_NORMAL), 441 'Cannot use C\D as B because the name is already in use on line 2' 442 ], 443 [ 444 new Stmt\Use_([ 445 new Node\UseItem(new Name('a\b'), 'b', 0, ['startLine' => 1]), 446 new Node\UseItem(new Name('c\d'), 'B', 0, ['startLine' => 2]), 447 ], Stmt\Use_::TYPE_FUNCTION), 448 'Cannot use function c\d as B because the name is already in use on line 2' 449 ], 450 [ 451 new Stmt\Use_([ 452 new Node\UseItem(new Name('A\B'), 'B', 0, ['startLine' => 1]), 453 new Node\UseItem(new Name('C\D'), 'B', 0, ['startLine' => 2]), 454 ], Stmt\Use_::TYPE_CONSTANT), 455 'Cannot use const C\D as B because the name is already in use on line 2' 456 ], 457 [ 458 new Expr\New_(new Name\FullyQualified('self', ['startLine' => 3])), 459 "'\\self' is an invalid class name on line 3" 460 ], 461 [ 462 new Expr\New_(new Name\Relative('self', ['startLine' => 3])), 463 "'\\self' is an invalid class name on line 3" 464 ], 465 [ 466 new Expr\New_(new Name\FullyQualified('PARENT', ['startLine' => 3])), 467 "'\\PARENT' is an invalid class name on line 3" 468 ], 469 [ 470 new Expr\New_(new Name\Relative('STATIC', ['startLine' => 3])), 471 "'\\STATIC' is an invalid class name on line 3" 472 ], 473 ]; 474 } 475 476 public function testClassNameIsCaseInsensitive(): void { 477 $source = <<<'EOC' 478<?php 479namespace Foo; 480use Bar\Baz; 481$test = new baz(); 482EOC; 483 484 $parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative()); 485 $stmts = $parser->parse($source); 486 487 $traverser = new PhpParser\NodeTraverser(); 488 $traverser->addVisitor(new NameResolver()); 489 490 $stmts = $traverser->traverse($stmts); 491 $stmt = $stmts[0]; 492 493 $assign = $stmt->stmts[1]->expr; 494 $this->assertSame('Bar\\Baz', $assign->expr->class->name); 495 } 496 497 public function testSpecialClassNamesAreCaseInsensitive(): void { 498 $source = <<<'EOC' 499<?php 500namespace Foo; 501 502class Bar 503{ 504 public static function method() 505 { 506 SELF::method(); 507 PARENT::method(); 508 STATIC::method(); 509 } 510} 511EOC; 512 513 $parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative()); 514 $stmts = $parser->parse($source); 515 516 $traverser = new PhpParser\NodeTraverser(); 517 $traverser->addVisitor(new NameResolver()); 518 519 $stmts = $traverser->traverse($stmts); 520 $classStmt = $stmts[0]; 521 $methodStmt = $classStmt->stmts[0]->stmts[0]; 522 523 $this->assertSame('SELF', (string) $methodStmt->stmts[0]->expr->class); 524 $this->assertSame('PARENT', (string) $methodStmt->stmts[1]->expr->class); 525 $this->assertSame('STATIC', (string) $methodStmt->stmts[2]->expr->class); 526 } 527 528 public function testAddOriginalNames(): void { 529 $traverser = new PhpParser\NodeTraverser(); 530 $traverser->addVisitor(new NameResolver(null, ['preserveOriginalNames' => true])); 531 532 $n1 = new Name('Bar'); 533 $n2 = new Name('bar'); 534 $origStmts = [ 535 new Stmt\Namespace_(new Name('Foo'), [ 536 new Expr\ClassConstFetch($n1, 'FOO'), 537 new Expr\FuncCall($n2), 538 ]) 539 ]; 540 541 $stmts = $traverser->traverse($origStmts); 542 543 $this->assertSame($n1, $stmts[0]->stmts[0]->class->getAttribute('originalName')); 544 $this->assertSame($n2, $stmts[0]->stmts[1]->name->getAttribute('originalName')); 545 } 546 547 public function testAttributeOnlyMode(): void { 548 $traverser = new PhpParser\NodeTraverser(); 549 $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); 550 551 $n1 = new Name('Bar'); 552 $n2 = new Name('bar'); 553 $origStmts = [ 554 new Stmt\Namespace_(new Name('Foo'), [ 555 new Expr\ClassConstFetch($n1, 'FOO'), 556 new Expr\FuncCall($n2), 557 ]) 558 ]; 559 560 $traverser->traverse($origStmts); 561 562 $this->assertEquals( 563 new Name\FullyQualified('Foo\Bar'), $n1->getAttribute('resolvedName')); 564 $this->assertFalse($n2->hasAttribute('resolvedName')); 565 $this->assertEquals( 566 new Name\FullyQualified('Foo\bar'), $n2->getAttribute('namespacedName')); 567 } 568 569 private function parseAndResolve(string $code): array { 570 $parser = new PhpParser\Parser\Php8(new PhpParser\Lexer\Emulative()); 571 $traverser = new PhpParser\NodeTraverser(); 572 $traverser->addVisitor(new NameResolver()); 573 574 $stmts = $parser->parse($code); 575 return $traverser->traverse($stmts); 576 } 577} 578