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