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