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