packages/deriver/tests/Unit/Deriver/Lowerer/Graph/ProgramTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Lowerer\Graph;
6
7use Deriver\Deriver\Evaluator\TargetProfile;
8use Deriver\Deriver\Lowerer\Declaration\Index;
9use Deriver\Deriver\Lowerer\Graph\Program;
10use Deriver\Deriver\Parser\Node\File;
11use Deriver\Deriver\Parser\Parser;
12use Deriver\Deriver\Parser\ParserConfig;
13use Deriver\Deriver\Resolver\Resolver;
14use Deriver\Deriver\Sources\Path;
15use Deriver\Deriver\Sources\Source;
16use Deriver\Deriver\Sources\Sources;
17use PHPUnit\Framework\Attributes\CoversNothing;
18use PHPUnit\Framework\Attributes\Small;
19use PHPUnit\Framework\TestCase;
20
21#[CoversNothing]
22#[Small]
23final class ProgramTest extends TestCase
24{
25    public function testCallablePreservesTheSemanticContract(): void
26    {
27        self::assertContains(Program::class, class_implements(Index::class));
28    }
29    public function testClassesIncludesDeclarationsBeforeAnyGraphIsDemanded(): void
30    {
31        $source = '<?php class A {}';
32        $snapshotId = 'test';
33        $input = new Sources([new Source('fixture.php', $source)]);
34        $profile = new TargetProfile();
35        $parser = new Parser(new ParserConfig($profile->version));
36        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
37        $index = new Index($snapshotId, $trees);
38        self::assertSame(['a'], array_keys($index->classes()));
39        self::assertSame(0, $index->graphCount());
40    }
41    public function testSymbolsReturnsStableSourceIdentities(): void
42    {
43        $source = '<?php function Z(){} function A(){}';
44        $snapshotId = 'test';
45        $input = new Sources([new Source('fixture.php', $source)]);
46        $profile = new TargetProfile();
47        $parser = new Parser(new ParserConfig($profile->version));
48        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
49        $index = new Index($snapshotId, $trees);
50        self::assertSame(['A', 'Z', 'script:fixture.php'], $index->symbols());
51    }
52    public function testDiagnosticsDistinguishesConflictingDeclarationsFromAnEmptyIndex(): void
53    {
54        $source = '<?php function same(){} function SAME(){}';
55        $snapshotId = 'test';
56        $input = new Sources([new Source('fixture.php', $source)]);
57        $profile = new TargetProfile();
58        $parser = new Parser(new ParserConfig($profile->version));
59        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
60        $index = new Index($snapshotId, $trees);
61        self::assertCount(1, $index->diagnostics());
62        self::assertSame('INVALID_PROGRAM', $index->diagnostics()[0]->code);
63        self::assertSame('duplicate:SAME', $index->diagnostics()[0]->operation);
64        self::assertSame('fixture.php', $index->diagnostics()[0]->at->path);
65        $source2 = '<?php';
66        $snapshotId1 = 'test';
67        $input1 = new Sources([new Source('fixture.php', $source2)]);
68        $profile1 = new TargetProfile();
69        $parser1 = new Parser(new ParserConfig($profile1->version));
70        $trees1 = array_map(static fn (Source $source3): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source3->path), $source3->contents, $source3->declarationsOnly))), iterator_to_array($input1, false));
71        self::assertSame([], (new Index($snapshotId1, $trees1))->diagnostics());
72    }
73    public function testGraphCountCountsOnlyDemandedBodiesAndReusesTheGraph(): void
74    {
75        $source = '<?php function target(){} function other(){}';
76        $snapshotId = 'test';
77        $input = new Sources([new Source('fixture.php', $source)]);
78        $profile = new TargetProfile();
79        $parser = new Parser(new ParserConfig($profile->version));
80        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
81        $index = new Index($snapshotId, $trees);
82        self::assertSame(0, $index->graphCount());
83        $first = $index->callable('target');
84        self::assertNotNull($first);
85        self::assertSame($first, $index->callable('TARGET'));
86        self::assertSame(1, $index->graphCount());
87    }
88    public function testConstantCompilesARequestedInitializerOnce(): void
89    {
90        $source = '<?php const ANSWER = 40+2;';
91        $snapshotId = 'test';
92        $input = new Sources([new Source('fixture.php', $source)]);
93        $profile = new TargetProfile();
94        $parser = new Parser(new ParserConfig($profile->version));
95        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
96        $index = new Index($snapshotId, $trees);
97        $constant = $index->constant('ANSWER');
98        self::assertNotNull($constant);
99        self::assertSame(['constant', 'constant', 'binary'], array_column($constant->blocks[0]->instructions, 'operation'));
100        self::assertSame($constant, $index->constant('ANSWER'));
101        self::assertNull($index->constant('answer'));
102    }
103    public function testCallOwnersDoesNotCompileUnrelatedBodies(): void
104    {
105        $source = '<?php function used(){sink(1);} function other(){unrelated();}';
106        $snapshotId = 'test';
107        $input = new Sources([new Source('fixture.php', $source)]);
108        $profile = new TargetProfile();
109        $parser = new Parser(new ParserConfig($profile->version));
110        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
111        $index = new Index($snapshotId, $trees);
112        self::assertSame(['used'], $index->callOwners('sink'));
113        self::assertSame(0, $index->graphCount());
114    }
115    public function testPropertyOwnersDoesNotLowerUnrelatedBodies(): void
116    {
117        $source = '<?php class A{public $x;function set(){ $this->x=1; }} function unrelated(){return 2;}';
118        $snapshotId = 'test';
119        $input = new Sources([new Source('fixture.php', $source)]);
120        $profile = new TargetProfile();
121        $parser = new Parser(new ParserConfig($profile->version));
122        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
123        $index = new Index($snapshotId, $trees);
124        self::assertSame(['A::set'], $index->propertyOwners('x'));
125        self::assertSame(0, $index->graphCount());
126    }
127    public function testVariantsRetainsBothConditionalDeclarations(): void
128    {
129        $source = '<?php if($x){function f(){return 1;}}else{function f(){return 2;}}';
130        $snapshotId = 'test';
131        $input = new Sources([new Source('fixture.php', $source)]);
132        $profile = new TargetProfile();
133        $parser = new Parser(new ParserConfig($profile->version));
134        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
135        $index = new Index($snapshotId, $trees);
136        self::assertCount(1, $index->variants('f'));
137        self::assertSame([], $index->diagnostics());
138    }
139    public function testHasSymbolPreservesExactSpellingWithoutCompiling(): void
140    {
141        $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('fixture.php', '<?php function Target(){}')));
142        $index = new Index('test', [$tree]);
143        self::assertTrue($index->hasSymbol('Target'));
144        self::assertFalse($index->hasSymbol('target'));
145        self::assertFalse($index->hasSymbol('missing'));
146        self::assertSame(0, $index->graphCount());
147    }
148
149    public function testClosureOwnersFindsTheLexicalParentBeforeLowering(): void
150    {
151        $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('fixture.php', '<?php function owner(){return fn()=>sink();}')));
152        $index = new Index('test', [$tree]);
153        $owners = $index->callOwners('sink');
154        self::assertCount(1, $owners);
155        self::assertSame(['owner'], $index->closureOwners($owners[0]));
156        self::assertSame(0, $index->graphCount());
157    }
158
159    public function testGlobalOwnersExcludesFunctionLocals(): void
160    {
161        $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('fixture.php', '<?php $x=1; function f(){$local=2;}')));
162        $index = new Index('test', [$tree]);
163        self::assertSame(['script:fixture.php'], $index->globalOwners('x'));
164        self::assertSame([], $index->globalOwners('local'));
165        self::assertSame(0, $index->graphCount());
166    }
167
168}
169