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