php-parser/test/PhpParser/NodeVisitor/NameResolverTest.php

533 lines
13 KiB
PHP
Raw Permalink Normal View History

<?php declare(strict_types=1);
namespace PhpParser\NodeVisitor;
use PhpParser;
use PhpParser\Node;
2016-11-23 22:58:18 +01:00
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
class NameResolverTest extends \PHPUnit\Framework\TestCase
{
private function canonicalize($string) {
return str_replace("\r\n", "\n", $string);
}
/**
* @covers \PhpParser\NodeVisitor\NameResolver
*/
public function testResolveNames() {
$code = <<<'EOC'
<?php
namespace Foo {
use Hallo as Hi;
new Bar();
new Hi();
new Hi\Bar();
new \Bar();
new namespace\Bar();
bar();
hi();
Hi\bar();
foo\bar();
\bar();
namespace\bar();
}
namespace {
use Hallo as Hi;
new Bar();
new Hi();
new Hi\Bar();
new \Bar();
new namespace\Bar();
bar();
hi();
Hi\bar();
foo\bar();
\bar();
namespace\bar();
}
namespace Bar {
use function foo\bar as baz;
use const foo\BAR as BAZ;
use foo as bar;
bar();
baz();
bar\foo();
baz\foo();
BAR();
BAZ();
BAR\FOO();
BAZ\FOO();
bar;
baz;
bar\foo;
baz\foo;
BAR;
BAZ;
BAR\FOO;
BAZ\FOO;
}
namespace Baz {
use A\T\{B\C, D\E};
use function X\T\{b\c, d\e};
use const Y\T\{B\C, D\E};
2015-06-13 11:27:38 +02:00
use Z\T\{G, function f, const K};
new C;
new E;
new C\D;
new E\F;
2015-06-13 11:27:38 +02:00
new G;
c();
e();
2015-06-13 11:27:38 +02:00
f();
C;
E;
2015-06-13 11:27:38 +02:00
K;
}
EOC;
$expectedCode = <<<'EOC'
namespace Foo {
use Hallo as Hi;
new \Foo\Bar();
new \Hallo();
new \Hallo\Bar();
new \Bar();
new \Foo\Bar();
bar();
hi();
\Hallo\bar();
\Foo\foo\bar();
\bar();
\Foo\bar();
}
namespace {
use Hallo as Hi;
new \Bar();
new \Hallo();
new \Hallo\Bar();
new \Bar();
new \Bar();
\bar();
\hi();
\Hallo\bar();
\foo\bar();
\bar();
\bar();
}
namespace Bar {
use function foo\bar as baz;
use const foo\BAR as BAZ;
use foo as bar;
bar();
\foo\bar();
\foo\foo();
\Bar\baz\foo();
BAR();
\foo\bar();
\foo\FOO();
\Bar\BAZ\FOO();
bar;
baz;
\foo\foo;
\Bar\baz\foo;
BAR;
\foo\BAR;
\foo\FOO;
\Bar\BAZ\FOO;
}
namespace Baz {
use A\T\{B\C, D\E};
use function X\T\{b\c, d\e};
use const Y\T\{B\C, D\E};
2015-06-13 11:27:38 +02:00
use Z\T\{G, function f, const K};
new \A\T\B\C();
new \A\T\D\E();
new \A\T\B\C\D();
new \A\T\D\E\F();
2015-06-13 11:27:38 +02:00
new \Z\T\G();
\X\T\b\c();
\X\T\d\e();
2015-06-13 11:27:38 +02:00
\Z\T\f();
\Y\T\B\C;
\Y\T\D\E;
2015-06-13 11:27:38 +02:00
\Z\T\K;
}
EOC;
$prettyPrinter = new PhpParser\PrettyPrinter\Standard;
$stmts = $this->parseAndResolve($code);
$this->assertSame(
$this->canonicalize($expectedCode),
$prettyPrinter->prettyPrint($stmts)
);
}
/**
* @covers \PhpParser\NodeVisitor\NameResolver
*/
public function testResolveLocations() {
$code = <<<'EOC'
<?php
namespace NS;
#[X]
class A extends B implements C, D {
use E, F, G {
f as private g;
E::h as i;
E::j insteadof F, G;
}
#[X]
public float $php = 7.4;
public ?Foo $person;
protected static ?bool $probability;
public A|B|int $prop;
#[X]
const C = 1;
}
#[X]
interface A extends C, D {
public function a(A $a) : A;
public function b(A|B|int $a): A|B|int;
}
#[X]
trait A {}
#[X]
function f(#[X] A $a) : A {}
function f2(array $a) : array {}
function fn3(?A $a) : ?A {}
function fn4(?array $a) : ?array {}
#[X]
function(A $a) : A {};
#[X]
fn(array $a): array => $a;
fn(A $a): A => $a;
fn(?A $a): ?A => $a;
A::b();
A::$b;
A::B;
new A;
$a instanceof A;
2012-05-06 17:58:31 +02:00
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;
}
#[\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;
}
#[\NS\X]
trait A
{
}
#[\NS\X]
function f(#[\NS\X] \NS\A $a) : \NS\A
{
}
function f2(array $a) : array
{
}
2016-12-06 12:26:21 +01:00
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() {
2017-08-13 14:35:03 +02:00
$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() {
2017-08-13 14:35:03 +02:00
$nsStmts = [
new Stmt\Class_('A'),
new Stmt\Interface_('B'),
new Stmt\Function_('C'),
2017-08-13 14:35:03 +02:00
new Stmt\Const_([
new Node\Const_('D', new Node\Scalar\LNumber(42))
2017-08-13 14:35:03 +02:00
]),
new Stmt\Trait_('E'),
new Expr\New_(new Stmt\Class_(null)),
2017-08-13 14:35:03 +02:00
];
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $traverser->traverse([new Stmt\Namespace_(new Name('NS'), $nsStmts)]);
2014-09-30 20:38:09 +02:00
$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->assertObjectNotHasAttribute('namespacedName', $stmts[0]->stmts[5]->class);
$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->assertObjectNotHasAttribute('namespacedName', $stmts[0]->stmts[5]->class);
}
public function testAddRuntimeResolvedNamespacedName() {
2017-08-13 14:35:03 +02:00
$stmts = [
new Stmt\Namespace_(new Name('NS'), [
new Expr\FuncCall(new Name('foo')),
new Expr\ConstFetch(new Name('FOO')),
2017-08-13 14:35:03 +02:00
]),
new Stmt\Namespace_(null, [
new Expr\FuncCall(new Name('foo')),
new Expr\ConstFetch(new Name('FOO')),
2017-08-13 14:35:03 +02:00
]),
];
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $traverser->traverse($stmts);
2017-08-13 14:35:03 +02:00
$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) {
$this->expectException(\PhpParser\Error::class);
2017-04-27 18:14:07 +02:00
$this->expectExceptionMessage($errorMsg);
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
2017-08-13 14:35:03 +02:00
$traverser->traverse([$stmt]);
}
public function provideTestError() {
2017-08-13 14:35:03 +02:00
return [
[
new Stmt\Use_([
new Stmt\UseUse(new Name('A\B'), 'B', 0, ['startLine' => 1]),
new Stmt\UseUse(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'
2017-08-13 14:35:03 +02:00
],
[
new Stmt\Use_([
new Stmt\UseUse(new Name('a\b'), 'b', 0, ['startLine' => 1]),
new Stmt\UseUse(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'
2017-08-13 14:35:03 +02:00
],
[
new Stmt\Use_([
new Stmt\UseUse(new Name('A\B'), 'B', 0, ['startLine' => 1]),
new Stmt\UseUse(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'
2017-08-13 14:35:03 +02:00
],
[
new Expr\New_(new Name\FullyQualified('self', ['startLine' => 3])),
"'\\self' is an invalid class name on line 3"
2017-08-13 14:35:03 +02:00
],
[
new Expr\New_(new Name\Relative('self', ['startLine' => 3])),
"'\\self' is an invalid class name on line 3"
2017-08-13 14:35:03 +02:00
],
[
new Expr\New_(new Name\FullyQualified('PARENT', ['startLine' => 3])),
"'\\PARENT' is an invalid class name on line 3"
2017-08-13 14:35:03 +02:00
],
[
new Expr\New_(new Name\Relative('STATIC', ['startLine' => 3])),
"'\\STATIC' is an invalid class name on line 3"
2017-08-13 14:35:03 +02:00
],
];
}
public function testClassNameIsCaseInsensitive()
{
$source = <<<'EOC'
<?php
namespace Foo;
use Bar\Baz;
$test = new baz();
EOC;
$parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative);
$stmts = $parser->parse($source);
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $traverser->traverse($stmts);
$stmt = $stmts[0];
$assign = $stmt->stmts[1]->expr;
2017-08-13 14:35:03 +02:00
$this->assertSame(['Bar', 'Baz'], $assign->expr->class->parts);
}
public function testSpecialClassNamesAreCaseInsensitive() {
$source = <<<'EOC'
<?php
namespace Foo;
class Bar
{
public static function method()
{
SELF::method();
PARENT::method();
STATIC::method();
}
}
EOC;
$parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative);
$stmts = $parser->parse($source);
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $traverser->traverse($stmts);
$classStmt = $stmts[0];
$methodStmt = $classStmt->stmts[0]->stmts[0];
2018-01-10 17:31:33 +01:00
$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() {
$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() {
$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\Php7(new PhpParser\Lexer\Emulative);
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $parser->parse($code);
return $traverser->traverse($stmts);
}
}