packages/deriver/tests/Unit/Deriver/Lowerer/Declaration/ReferencesTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Deriver\Lowerer\Declaration;
6
7use Deriver\Deriver\Lowerer\Declaration\Index;
8use Deriver\Deriver\Parser\Parser;
9use Deriver\Deriver\Parser\ParserConfig;
10use Deriver\Deriver\Resolver\Resolver;
11use Deriver\Deriver\Sources\Source;
12use PHPUnit\Framework\Attributes\CoversNothing;
13use PHPUnit\Framework\TestCase;
14
15#[CoversNothing]
16final class ReferencesTest extends TestCase
17{
18    public function testAddIndexesNestedClosuresWithoutLoweringAnyBody(): void
19    {
20        $source = new Source('fixture.php', '<?php function outer(){ $a=1; return fn()=>function()use($a){return sink($a);}; } function unrelated(){other();}');
21        $index = new Index('test', [(new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse($source))]);
22        $owners = $index->callOwners('sink');
23        self::assertCount(1, $owners);
24        $parents = $index->closureOwners($owners[0]);
25        self::assertCount(1, $parents);
26        self::assertSame(['outer'], $index->closureOwners($parents[0]));
27        self::assertSame([], $index->closureOwners('missing'));
28        self::assertSame(0, $index->graphCount());
29    }
30
31    public function testCallsRetainNamespaceFallbacksAndLexicalConstructors(): void
32    {
33        $source = new Source('fixture.php', '<?php namespace N; use function M\\sink as imported; function a(){sink();} function b(){imported();} class C {function create(){return new self;} function other(){return new Other;}}');
34        $index = new Index('test', [(new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse($source))]);
35        self::assertSame(['N\\a'], $index->callOwners('sink'));
36        self::assertSame(['N\\a'], $index->callOwners('\\n\\SINK'));
37        self::assertSame(['N\\b'], $index->callOwners('M\\sink'));
38        self::assertSame(['N\\C::create'], $index->callOwners('N\\C::__construct'));
39        self::assertSame(['N\\C::create', 'N\\C::other'], $index->callOwners('N\\Other::__construct'));
40        self::assertSame([], $index->callOwners('absent'));
41        self::assertSame(0, $index->graphCount());
42    }
43
44    public function testPropertiesPreserveDynamicNamesAndClosureBoundaries(): void
45    {
46        $source = new Source('fixture.php', '<?php class C {function __construct(public $x){} function dynamic($n){$this->$n=1;} function nested(){return function(){$this->x[0]+=1;};} function uppercase(){$this->X=2;}}');
47        $index = new Index('test', [(new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse($source))]);
48        $owners = $index->propertyOwners('x');
49        self::assertCount(3, $owners);
50        self::assertContains('C::__construct', $owners);
51        self::assertContains('C::dynamic', $owners);
52        self::assertNotContains('C::nested', $owners);
53        self::assertNotContains('C::uppercase', $owners);
54        self::assertSame(['C::dynamic', 'C::uppercase'], $index->propertyOwners('X'));
55        self::assertSame(['C::dynamic'], $index->propertyOwners('absent'));
56        self::assertSame(0, $index->graphCount());
57    }
58
59    public function testGlobalsDoNotIncludeLocalsInFunctionsOrClosures(): void
60    {
61        $parser = new Parser(new ParserConfig('8.3'));
62        $sources = [new Source('a.php', '<?php $wanted=1;'), new Source('b.php', '<?php function f(){$wanted=2;} $c=function(){$wanted=3;};'), new Source('c.php', '<?php $$name=4;')];
63        $index = new Index('test', array_map(static fn (Source $source) => (new Resolver())->resolve($parser->parse($source)), $sources));
64        self::assertSame(['script:a.php', 'script:c.php'], $index->globalOwners('wanted'));
65        self::assertSame(['script:c.php'], $index->globalOwners('absent'));
66        self::assertSame(0, $index->graphCount());
67    }
68
69    public function testNestedAnonymousMethodsExtendTheIndexesWithoutRebuildingOtherBodies(): void
70    {
71        $source = new Source('fixture.php', '<?php function make(){return new class {function run(){return sink();}};} function unrelated(){other();}');
72        $index = new Index('test', [(new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse($source))]);
73        $before = $index->callOwners('sink');
74        $graph = $index->callable('make');
75        $owners = $index->callOwners('sink');
76        self::assertSame([], $before);
77        self::assertNotNull($graph);
78        self::assertCount(1, $owners);
79        self::assertStringEndsWith('::run', $owners[0]);
80        self::assertSame(1, $index->graphCount());
81    }
82
83    public function testClosuresKeepTraitConsumerScopesSeparate(): void
84    {
85        $source = new Source('fixture.php', '<?php trait T {function make(){return fn()=>sink();}} class A {use T;} class B {use T;}');
86        $index = new Index('test', [(new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse($source))]);
87        $owners = $index->callOwners('sink');
88        self::assertCount(3, $owners);
89        $parents = array_map($index->closureOwners(...), $owners);
90        self::assertContains(['A::make'], $parents);
91        self::assertContains(['B::make'], $parents);
92        self::assertContains(['T::make'], $parents);
93        self::assertSame(0, $index->graphCount());
94    }
95
96    public function testPropertyIndexesMutationFormsWithoutReadOnlyUses(): void
97    {
98        $source = new Source('fixture.php', '<?php class C {function change(){++$this->x; $this->y+=1; self::$z[0]=2;} function read(){return $this->x;}}');
99        $index = new Index('test', [(new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse($source))]);
100        self::assertSame(['C::change'], $index->propertyOwners('x'));
101        self::assertSame(['C::change'], $index->propertyOwners('y'));
102        self::assertSame(['C::change'], $index->propertyOwners('z'));
103    }
104}
105