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() { 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() { 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 206#[X] 207interface A extends C, D { 208 public function a(A $a) : A; 209 public function b(A|B|int $a): A|B|int; 210 public function c(A&B $a): A&B; 211} 212 213#[X] 214enum E: int { 215 #[X] 216 case A = 1; 217} 218 219#[X] 220trait A {} 221 222#[X] 223function f(#[X] A $a) : A {} 224function f2(array $a) : array {} 225function fn3(?A $a) : ?A {} 226function fn4(?array $a) : ?array {} 227 228#[X] 229function(A $a) : A {}; 230 231#[X] 232fn(array $a): array => $a; 233fn(A $a): A => $a; 234fn(?A $a): ?A => $a; 235 236A::b(); 237A::$b; 238A::B; 239new A; 240$a instanceof A; 241 242namespace\a(); 243namespace\A; 244 245try { 246 $someThing; 247} catch (A $a) { 248 $someThingElse; 249} 250EOC; 251 $expectedCode = <<<'EOC' 252namespace NS; 253 254#[\NS\X] 255class A extends \NS\B implements \NS\C, \NS\D 256{ 257 use \NS\E, \NS\F, \NS\G { 258 f as private g; 259 \NS\E::h as i; 260 \NS\E::j insteadof \NS\F, \NS\G; 261 } 262 #[\NS\X] 263 public float $php = 7.4; 264 public ?\NS\Foo $person; 265 protected static ?bool $probability; 266 public \NS\A|\NS\B|int $prop; 267 #[\NS\X] 268 const C = 1; 269 public const \NS\X A = \NS\X::Bar; 270 public const \NS\X\Foo B = \NS\X\Foo::Bar; 271 public const \X\Foo C = \X\Foo::Bar; 272} 273#[\NS\X] 274interface A extends \NS\C, \NS\D 275{ 276 public function a(\NS\A $a): \NS\A; 277 public function b(\NS\A|\NS\B|int $a): \NS\A|\NS\B|int; 278 public function c(\NS\A&\NS\B $a): \NS\A&\NS\B; 279} 280#[\NS\X] 281enum E : int 282{ 283 #[\NS\X] 284 case A = 1; 285} 286#[\NS\X] 287trait A 288{ 289} 290#[\NS\X] 291function f(#[\NS\X] \NS\A $a): \NS\A 292{ 293} 294function f2(array $a): array 295{ 296} 297function fn3(?\NS\A $a): ?\NS\A 298{ 299} 300function fn4(?array $a): ?array 301{ 302} 303#[\NS\X] function (\NS\A $a): \NS\A { 304}; 305#[\NS\X] fn(array $a): array => $a; 306fn(\NS\A $a): \NS\A => $a; 307fn(?\NS\A $a): ?\NS\A => $a; 308\NS\A::b(); 309\NS\A::$b; 310\NS\A::B; 311new \NS\A(); 312$a instanceof \NS\A; 313\NS\a(); 314\NS\A; 315try { 316 $someThing; 317} catch (\NS\A $a) { 318 $someThingElse; 319} 320EOC; 321 322 $prettyPrinter = new PhpParser\PrettyPrinter\Standard(); 323 $stmts = $this->parseAndResolve($code); 324 325 $this->assertSame( 326 $this->canonicalize($expectedCode), 327 $prettyPrinter->prettyPrint($stmts) 328 ); 329 } 330 331 public function testNoResolveSpecialName() { 332 $stmts = [new Node\Expr\New_(new Name('self'))]; 333 334 $traverser = new PhpParser\NodeTraverser(); 335 $traverser->addVisitor(new NameResolver()); 336 337 $this->assertEquals($stmts, $traverser->traverse($stmts)); 338 } 339 340 public function testAddDeclarationNamespacedName() { 341 $nsStmts = [ 342 new Stmt\Class_('A'), 343 new Stmt\Interface_('B'), 344 new Stmt\Function_('C'), 345 new Stmt\Const_([ 346 new Node\Const_('D', new Node\Scalar\Int_(42)) 347 ]), 348 new Stmt\Trait_('E'), 349 new Expr\New_(new Stmt\Class_(null)), 350 new Stmt\Enum_('F'), 351 ]; 352 353 $traverser = new PhpParser\NodeTraverser(); 354 $traverser->addVisitor(new NameResolver()); 355 356 $stmts = $traverser->traverse([new Stmt\Namespace_(new Name('NS'), $nsStmts)]); 357 $this->assertSame('NS\\A', (string) $stmts[0]->stmts[0]->namespacedName); 358 $this->assertSame('NS\\B', (string) $stmts[0]->stmts[1]->namespacedName); 359 $this->assertSame('NS\\C', (string) $stmts[0]->stmts[2]->namespacedName); 360 $this->assertSame('NS\\D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName); 361 $this->assertSame('NS\\E', (string) $stmts[0]->stmts[4]->namespacedName); 362 $this->assertNull($stmts[0]->stmts[5]->class->namespacedName); 363 $this->assertSame('NS\\F', (string) $stmts[0]->stmts[6]->namespacedName); 364 365 $stmts = $traverser->traverse([new Stmt\Namespace_(null, $nsStmts)]); 366 $this->assertSame('A', (string) $stmts[0]->stmts[0]->namespacedName); 367 $this->assertSame('B', (string) $stmts[0]->stmts[1]->namespacedName); 368 $this->assertSame('C', (string) $stmts[0]->stmts[2]->namespacedName); 369 $this->assertSame('D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName); 370 $this->assertSame('E', (string) $stmts[0]->stmts[4]->namespacedName); 371 $this->assertNull($stmts[0]->stmts[5]->class->namespacedName); 372 $this->assertSame('F', (string) $stmts[0]->stmts[6]->namespacedName); 373 } 374 375 public function testAddRuntimeResolvedNamespacedName() { 376 $stmts = [ 377 new Stmt\Namespace_(new Name('NS'), [ 378 new Expr\FuncCall(new Name('foo')), 379 new Expr\ConstFetch(new Name('FOO')), 380 ]), 381 new Stmt\Namespace_(null, [ 382 new Expr\FuncCall(new Name('foo')), 383 new Expr\ConstFetch(new Name('FOO')), 384 ]), 385 ]; 386 387 $traverser = new PhpParser\NodeTraverser(); 388 $traverser->addVisitor(new NameResolver()); 389 $stmts = $traverser->traverse($stmts); 390 391 $this->assertSame('NS\\foo', (string) $stmts[0]->stmts[0]->name->getAttribute('namespacedName')); 392 $this->assertSame('NS\\FOO', (string) $stmts[0]->stmts[1]->name->getAttribute('namespacedName')); 393 394 $this->assertFalse($stmts[1]->stmts[0]->name->hasAttribute('namespacedName')); 395 $this->assertFalse($stmts[1]->stmts[1]->name->hasAttribute('namespacedName')); 396 } 397 398 /** 399 * @dataProvider provideTestError 400 */ 401 public function testError(Node $stmt, $errorMsg) { 402 $this->expectException(\PhpParser\Error::class); 403 $this->expectExceptionMessage($errorMsg); 404 405 $traverser = new PhpParser\NodeTraverser(); 406 $traverser->addVisitor(new NameResolver()); 407 $traverser->traverse([$stmt]); 408 } 409 410 public function provideTestError() { 411 return [ 412 [ 413 new Stmt\Use_([ 414 new Node\UseItem(new Name('A\B'), 'B', 0, ['startLine' => 1]), 415 new Node\UseItem(new Name('C\D'), 'B', 0, ['startLine' => 2]), 416 ], Stmt\Use_::TYPE_NORMAL), 417 'Cannot use C\D as B because the name is already in use on line 2' 418 ], 419 [ 420 new Stmt\Use_([ 421 new Node\UseItem(new Name('a\b'), 'b', 0, ['startLine' => 1]), 422 new Node\UseItem(new Name('c\d'), 'B', 0, ['startLine' => 2]), 423 ], Stmt\Use_::TYPE_FUNCTION), 424 'Cannot use function c\d as B because the name is already in use on line 2' 425 ], 426 [ 427 new Stmt\Use_([ 428 new Node\UseItem(new Name('A\B'), 'B', 0, ['startLine' => 1]), 429 new Node\UseItem(new Name('C\D'), 'B', 0, ['startLine' => 2]), 430 ], Stmt\Use_::TYPE_CONSTANT), 431 'Cannot use const C\D as B because the name is already in use on line 2' 432 ], 433 [ 434 new Expr\New_(new Name\FullyQualified('self', ['startLine' => 3])), 435 "'\\self' is an invalid class name on line 3" 436 ], 437 [ 438 new Expr\New_(new Name\Relative('self', ['startLine' => 3])), 439 "'\\self' is an invalid class name on line 3" 440 ], 441 [ 442 new Expr\New_(new Name\FullyQualified('PARENT', ['startLine' => 3])), 443 "'\\PARENT' is an invalid class name on line 3" 444 ], 445 [ 446 new Expr\New_(new Name\Relative('STATIC', ['startLine' => 3])), 447 "'\\STATIC' is an invalid class name on line 3" 448 ], 449 ]; 450 } 451 452 public function testClassNameIsCaseInsensitive() { 453 $source = <<<'EOC' 454<?php 455namespace Foo; 456use Bar\Baz; 457$test = new baz(); 458EOC; 459 460 $parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative()); 461 $stmts = $parser->parse($source); 462 463 $traverser = new PhpParser\NodeTraverser(); 464 $traverser->addVisitor(new NameResolver()); 465 466 $stmts = $traverser->traverse($stmts); 467 $stmt = $stmts[0]; 468 469 $assign = $stmt->stmts[1]->expr; 470 $this->assertSame('Bar\\Baz', $assign->expr->class->name); 471 } 472 473 public function testSpecialClassNamesAreCaseInsensitive() { 474 $source = <<<'EOC' 475<?php 476namespace Foo; 477 478class Bar 479{ 480 public static function method() 481 { 482 SELF::method(); 483 PARENT::method(); 484 STATIC::method(); 485 } 486} 487EOC; 488 489 $parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative()); 490 $stmts = $parser->parse($source); 491 492 $traverser = new PhpParser\NodeTraverser(); 493 $traverser->addVisitor(new NameResolver()); 494 495 $stmts = $traverser->traverse($stmts); 496 $classStmt = $stmts[0]; 497 $methodStmt = $classStmt->stmts[0]->stmts[0]; 498 499 $this->assertSame('SELF', (string) $methodStmt->stmts[0]->expr->class); 500 $this->assertSame('PARENT', (string) $methodStmt->stmts[1]->expr->class); 501 $this->assertSame('STATIC', (string) $methodStmt->stmts[2]->expr->class); 502 } 503 504 public function testAddOriginalNames() { 505 $traverser = new PhpParser\NodeTraverser(); 506 $traverser->addVisitor(new NameResolver(null, ['preserveOriginalNames' => true])); 507 508 $n1 = new Name('Bar'); 509 $n2 = new Name('bar'); 510 $origStmts = [ 511 new Stmt\Namespace_(new Name('Foo'), [ 512 new Expr\ClassConstFetch($n1, 'FOO'), 513 new Expr\FuncCall($n2), 514 ]) 515 ]; 516 517 $stmts = $traverser->traverse($origStmts); 518 519 $this->assertSame($n1, $stmts[0]->stmts[0]->class->getAttribute('originalName')); 520 $this->assertSame($n2, $stmts[0]->stmts[1]->name->getAttribute('originalName')); 521 } 522 523 public function testAttributeOnlyMode() { 524 $traverser = new PhpParser\NodeTraverser(); 525 $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); 526 527 $n1 = new Name('Bar'); 528 $n2 = new Name('bar'); 529 $origStmts = [ 530 new Stmt\Namespace_(new Name('Foo'), [ 531 new Expr\ClassConstFetch($n1, 'FOO'), 532 new Expr\FuncCall($n2), 533 ]) 534 ]; 535 536 $traverser->traverse($origStmts); 537 538 $this->assertEquals( 539 new Name\FullyQualified('Foo\Bar'), $n1->getAttribute('resolvedName')); 540 $this->assertFalse($n2->hasAttribute('resolvedName')); 541 $this->assertEquals( 542 new Name\FullyQualified('Foo\bar'), $n2->getAttribute('namespacedName')); 543 } 544 545 private function parseAndResolve(string $code): array { 546 $parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative()); 547 $traverser = new PhpParser\NodeTraverser(); 548 $traverser->addVisitor(new NameResolver()); 549 550 $stmts = $parser->parse($code); 551 return $traverser->traverse($stmts); 552 } 553} 554