parseAndResolve($code); $this->assertSame( $this->canonicalize($expectedCode), $prettyPrinter->prettyPrint($stmts) ); } /** * @covers \PhpParser\NodeVisitor\NameResolver */ public function testResolveLocations(): void { $code = <<<'EOC' $a; fn(A $a): A => $a; fn(?A $a): ?A => $a; A::b(); A::$b; A::B; new A; $a instanceof A; namespace\a(); namespace\A; try { $someThing; } catch (A $a) { $someThingElse; } EOC; $expectedCode = <<<'EOC' namespace NS; #[\NS\X] class A extends \NS\B implements \NS\C, \NS\D { use \NS\E, \NS\F, \NS\G { f as private g; \NS\E::h as i; \NS\E::j insteadof \NS\F, \NS\G; } #[\NS\X] public float $php = 7.4; public ?\NS\Foo $person; protected static ?bool $probability; public \NS\A|\NS\B|int $prop; #[\NS\X] const C = 1; public const \NS\X A = \NS\X::Bar; public const \NS\X\Foo B = \NS\X\Foo::Bar; public const \X\Foo C = \X\Foo::Bar; public \NS\Foo $foo { #[\NS\X] set(#[\NS\X] \NS\Bar $v) { } } public function __construct(public \NS\Foo $bar { #[\NS\X] set(#[\NS\X] \NS\Bar $v) { } }) { } } #[\NS\X] interface A extends \NS\C, \NS\D { public function a(\NS\A $a): \NS\A; public function b(\NS\A|\NS\B|int $a): \NS\A|\NS\B|int; public function c(\NS\A&\NS\B $a): \NS\A&\NS\B; } #[\NS\X] enum E : int { #[\NS\X] case A = 1; } #[\NS\X] trait A { } #[\NS\X] function f(#[\NS\X] \NS\A $a): \NS\A { } function f2(array $a): array { } function fn3(?\NS\A $a): ?\NS\A { } function fn4(?array $a): ?array { } #[\NS\X] function (\NS\A $a): \NS\A { }; #[\NS\X] fn(array $a): array => $a; fn(\NS\A $a): \NS\A => $a; fn(?\NS\A $a): ?\NS\A => $a; \NS\A::b(); \NS\A::$b; \NS\A::B; new \NS\A(); $a instanceof \NS\A; \NS\a(); \NS\A; try { $someThing; } catch (\NS\A $a) { $someThingElse; } EOC; $prettyPrinter = new PhpParser\PrettyPrinter\Standard(); $stmts = $this->parseAndResolve($code); $this->assertSame( $this->canonicalize($expectedCode), $prettyPrinter->prettyPrint($stmts) ); } public function testNoResolveSpecialName(): void { $stmts = [new Node\Expr\New_(new Name('self'))]; $traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor(new NameResolver()); $this->assertEquals($stmts, $traverser->traverse($stmts)); } public function testAddDeclarationNamespacedName(): void { $nsStmts = [ new Stmt\Class_('A'), new Stmt\Interface_('B'), new Stmt\Function_('C'), new Stmt\Const_([ new Node\Const_('D', new Node\Scalar\Int_(42)) ]), new Stmt\Trait_('E'), new Expr\New_(new Stmt\Class_(null)), new Stmt\Enum_('F'), ]; $traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor(new NameResolver()); $stmts = $traverser->traverse([new Stmt\Namespace_(new Name('NS'), $nsStmts)]); $this->assertSame('NS\\A', (string) $stmts[0]->stmts[0]->namespacedName); $this->assertSame('NS\\B', (string) $stmts[0]->stmts[1]->namespacedName); $this->assertSame('NS\\C', (string) $stmts[0]->stmts[2]->namespacedName); $this->assertSame('NS\\D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName); $this->assertSame('NS\\E', (string) $stmts[0]->stmts[4]->namespacedName); $this->assertNull($stmts[0]->stmts[5]->class->namespacedName); $this->assertSame('NS\\F', (string) $stmts[0]->stmts[6]->namespacedName); $stmts = $traverser->traverse([new Stmt\Namespace_(null, $nsStmts)]); $this->assertSame('A', (string) $stmts[0]->stmts[0]->namespacedName); $this->assertSame('B', (string) $stmts[0]->stmts[1]->namespacedName); $this->assertSame('C', (string) $stmts[0]->stmts[2]->namespacedName); $this->assertSame('D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName); $this->assertSame('E', (string) $stmts[0]->stmts[4]->namespacedName); $this->assertNull($stmts[0]->stmts[5]->class->namespacedName); $this->assertSame('F', (string) $stmts[0]->stmts[6]->namespacedName); } public function testAddRuntimeResolvedNamespacedName(): void { $stmts = [ new Stmt\Namespace_(new Name('NS'), [ new Expr\FuncCall(new Name('foo')), new Expr\ConstFetch(new Name('FOO')), ]), new Stmt\Namespace_(null, [ new Expr\FuncCall(new Name('foo')), new Expr\ConstFetch(new Name('FOO')), ]), ]; $traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor(new NameResolver()); $stmts = $traverser->traverse($stmts); $this->assertSame('NS\\foo', (string) $stmts[0]->stmts[0]->name->getAttribute('namespacedName')); $this->assertSame('NS\\FOO', (string) $stmts[0]->stmts[1]->name->getAttribute('namespacedName')); $this->assertFalse($stmts[1]->stmts[0]->name->hasAttribute('namespacedName')); $this->assertFalse($stmts[1]->stmts[1]->name->hasAttribute('namespacedName')); } /** * @dataProvider provideTestError */ public function testError(Node $stmt, $errorMsg): void { $this->expectException(\PhpParser\Error::class); $this->expectExceptionMessage($errorMsg); $traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor(new NameResolver()); $traverser->traverse([$stmt]); } public static function provideTestError() { return [ [ new Stmt\Use_([ new Node\UseItem(new Name('A\B'), 'B', 0, ['startLine' => 1]), new Node\UseItem(new Name('C\D'), 'B', 0, ['startLine' => 2]), ], Stmt\Use_::TYPE_NORMAL), 'Cannot use C\D as B because the name is already in use on line 2' ], [ new Stmt\Use_([ new Node\UseItem(new Name('a\b'), 'b', 0, ['startLine' => 1]), new Node\UseItem(new Name('c\d'), 'B', 0, ['startLine' => 2]), ], Stmt\Use_::TYPE_FUNCTION), 'Cannot use function c\d as B because the name is already in use on line 2' ], [ new Stmt\Use_([ new Node\UseItem(new Name('A\B'), 'B', 0, ['startLine' => 1]), new Node\UseItem(new Name('C\D'), 'B', 0, ['startLine' => 2]), ], Stmt\Use_::TYPE_CONSTANT), 'Cannot use const C\D as B because the name is already in use on line 2' ], [ new Expr\New_(new Name\FullyQualified('self', ['startLine' => 3])), "'\\self' is an invalid class name on line 3" ], [ new Expr\New_(new Name\Relative('self', ['startLine' => 3])), "'\\self' is an invalid class name on line 3" ], [ new Expr\New_(new Name\FullyQualified('PARENT', ['startLine' => 3])), "'\\PARENT' is an invalid class name on line 3" ], [ new Expr\New_(new Name\Relative('STATIC', ['startLine' => 3])), "'\\STATIC' is an invalid class name on line 3" ], ]; } public function testClassNameIsCaseInsensitive(): void { $source = <<<'EOC' parse($source); $traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor(new NameResolver()); $stmts = $traverser->traverse($stmts); $stmt = $stmts[0]; $assign = $stmt->stmts[1]->expr; $this->assertSame('Bar\\Baz', $assign->expr->class->name); } public function testSpecialClassNamesAreCaseInsensitive(): void { $source = <<<'EOC' parse($source); $traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor(new NameResolver()); $stmts = $traverser->traverse($stmts); $classStmt = $stmts[0]; $methodStmt = $classStmt->stmts[0]->stmts[0]; $this->assertSame('SELF', (string) $methodStmt->stmts[0]->expr->class); $this->assertSame('PARENT', (string) $methodStmt->stmts[1]->expr->class); $this->assertSame('STATIC', (string) $methodStmt->stmts[2]->expr->class); } public function testAddOriginalNames(): void { $traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor(new NameResolver(null, ['preserveOriginalNames' => true])); $n1 = new Name('Bar'); $n2 = new Name('bar'); $origStmts = [ new Stmt\Namespace_(new Name('Foo'), [ new Expr\ClassConstFetch($n1, 'FOO'), new Expr\FuncCall($n2), ]) ]; $stmts = $traverser->traverse($origStmts); $this->assertSame($n1, $stmts[0]->stmts[0]->class->getAttribute('originalName')); $this->assertSame($n2, $stmts[0]->stmts[1]->name->getAttribute('originalName')); } public function testAttributeOnlyMode(): void { $traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); $n1 = new Name('Bar'); $n2 = new Name('bar'); $origStmts = [ new Stmt\Namespace_(new Name('Foo'), [ new Expr\ClassConstFetch($n1, 'FOO'), new Expr\FuncCall($n2), ]) ]; $traverser->traverse($origStmts); $this->assertEquals( new Name\FullyQualified('Foo\Bar'), $n1->getAttribute('resolvedName')); $this->assertFalse($n2->hasAttribute('resolvedName')); $this->assertEquals( new Name\FullyQualified('Foo\bar'), $n2->getAttribute('namespacedName')); } private function parseAndResolve(string $code): array { $parser = new PhpParser\Parser\Php8(new PhpParser\Lexer\Emulative()); $traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor(new NameResolver()); $stmts = $parser->parse($code); return $traverser->traverse($stmts); } }