packages/deriver/tests/Unit/Deriver/Evaluator/Index/BodyIndexTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Index;
6
7use Deriver\Deriver\Contributions;
8use Deriver\Deriver\DeriverConfig;
9use Deriver\Deriver\Evaluator\Call\Evaluators;
10use Deriver\Deriver\Evaluator\Call\Registrations;
11use Deriver\Deriver\Evaluator\Evaluator;
12use Deriver\Deriver\Evaluator\EvaluatorConfig;
13use Deriver\Deriver\Evaluator\Evidence\Frontier;
14use Deriver\Deriver\Evaluator\Index\BodyIndex;
15use Deriver\Deriver\Evaluator\Index\DefinitionIndex;
16use Deriver\Deriver\Evaluator\Request\Budget;
17use Deriver\Deriver\Evaluator\Request\ReturnQuery;
18use Deriver\Deriver\Evaluator\Scope\Context;
19use Deriver\Deriver\Evaluator\Scope\Frame;
20use Deriver\Deriver\Evaluator\TargetProfile;
21use Deriver\Deriver\Evaluator\Term;
22use Deriver\Deriver\Lowerer\Declaration\Index;
23use Deriver\Deriver\MicroEvaluatorConversion;
24use Deriver\Deriver\Parser\Node\File;
25use Deriver\Deriver\Parser\Parser;
26use Deriver\Deriver\Parser\ParserConfig;
27use Deriver\Deriver\Providers\ProviderExpansion;
28use Deriver\Deriver\Publication\CandidateQuery;
29use Deriver\Deriver\Resolver\Resolver;
30use Deriver\Deriver\SnapshotIdentity;
31use Deriver\Deriver\Sources\Path;
32use Deriver\Deriver\Sources\Source;
33use Deriver\Deriver\Sources\Sources;
34use PHPUnit\Framework\Attributes\CoversNothing;
35use PHPUnit\Framework\TestCase;
36
37#[CoversNothing]
38final class BodyIndexTest extends TestCase
39{
40 public function testReachableBlocksIncludesExceptionHandlerEntries(): void
41 {
42 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('handlers.php', '<?php function target(){try{throw new Exception();}catch(Exception $e){return 1;}finally{return 2;}}')));
43 $index = new DefinitionIndex(new Index('handlers', [$tree]));
44 $graph = $index->graph('target');
45 self::assertNotNull($graph);
46 $region = $graph->body->regions[0];
47 self::assertNotNull($region->finally);
48 self::assertArrayHasKey($region->catches[0]->block, $graph->reachableBlocks());
49 self::assertArrayHasKey($region->finally, $graph->reachableBlocks());
50 }
51
52 public function testReturnsIndexesAllReturnOriginsWithoutEvaluatingThem(): void
53 {
54 $source = 'function target($flag){if($flag){return 1;}return external();}';
55 $configuration = new DeriverConfig();
56 $budget = new Budget();
57 $contributions = new Contributions();
58 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
59 $providers = new ProviderExpansion(new Sources([new Source('candidate.php', '<?php ' . $source)]), array_map(Term::fromNative(...), $configuration->environment), $contributions);
60 $contributions1 = $providers->contributions;
61 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
62 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
63 $snapshotId = 'test';
64 $input = $providers->input;
65 $parser = new Parser(new ParserConfig($target->version));
66 $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));
67 $program = new Index($snapshotId, $trees);
68 $e = new Evaluator(new Context(new DefinitionIndex($program), $settings, $evaluators, $budget));
69 $symbol = 'target';
70 $graph = $e->context->index->graph($symbol);
71 self::assertNotNull($graph);
72 $f = new Frame($graph, 'source:' . $symbol);
73 self::assertCount(2, $f->graph->returns());
74 self::assertSame(0, $e->context->bodyExpansions);
75 }
76 public function testReturnsPreservesTheDefinitionBeforeFinally(): void
77 {
78 $source = 'function target(){try{return 1;}finally{return 2;}}';
79 $configuration = new DeriverConfig();
80 $budget = new Budget();
81 $contributions = new Contributions();
82 $prepared1Input = new Sources([new Source('candidate.php', '<?php ' . $source)]);
83 $prepared1Configuration = $configuration;
84 $prepared1Contributions = $contributions;
85 $prepared1Target = new TargetProfile($prepared1Configuration->phpVersion, $prepared1Configuration->integerBits, $prepared1Configuration->floatPrecision);
86 $prepared1Providers = new ProviderExpansion($prepared1Input, array_map(Term::fromNative(...), $prepared1Configuration->environment), $prepared1Contributions);
87 $contributions1 = $prepared1Providers->contributions;
88 $prepared1Evaluators = new Evaluators(new TargetProfile($prepared1Configuration->phpVersion, $prepared1Configuration->integerBits, $prepared1Configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $prepared1Configuration->standardEvaluators);
89 $prepared1Settings = new EvaluatorConfig($prepared1Target, $prepared1Providers->environment, $prepared1Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared1Configuration->evaluators));
90 $prepared1Identity = new SnapshotIdentity($prepared1Providers->input, $prepared1Settings, $prepared1Evaluators, $prepared1Providers->manifest);
91 $snapshotId = $prepared1Identity->id;
92 $input = $prepared1Providers->input;
93 $parser = new Parser(new ParserConfig($prepared1Target->version));
94 $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));
95 $prepared1Program = new Index($snapshotId, $trees);
96 $prepared1Snapshot = $prepared1Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared1Program->diagnostics()));
97 self::assertSame(2, (new CandidateQuery(new DefinitionIndex($prepared1Program), $prepared1Settings, $prepared1Evaluators, $prepared1Snapshot))->derive(new ReturnQuery('target', budget: $budget))[0]->result);
98 }
99 public function testWritesIncludesEveryDirectStorageMutation(): void
100 {
101 $source = 'function target(){$x=1;$x+=2;$x++;unset($x);}';
102 $configuration = new DeriverConfig();
103 $budget = new Budget();
104 $contributions = new Contributions();
105 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
106 $providers = new ProviderExpansion(new Sources([new Source('candidate.php', '<?php ' . $source)]), array_map(Term::fromNative(...), $configuration->environment), $contributions);
107 $contributions1 = $providers->contributions;
108 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
109 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
110 $snapshotId = 'test';
111 $input = $providers->input;
112 $parser = new Parser(new ParserConfig($target->version));
113 $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));
114 $program = new Index($snapshotId, $trees);
115 $evaluator = new Evaluator(new Context(new DefinitionIndex($program), $settings, $evaluators, $budget));
116 $symbol = 'target';
117 $graph = $evaluator->context->index->graph($symbol);
118 self::assertNotNull($graph);
119 $frame = new Frame($graph, 'source:' . $symbol);
120 $operation = 'write';
121 $instruction = array_values(array_filter($frame->graph->definitions, static fn ($instruction): bool => $instruction->operation === $operation))[0] ?? null;
122 self::assertNotNull($instruction);
123 self::assertTrue(BodyIndex::writes($instruction));
124 $operation1 = 'compound';
125 $instruction1 = array_values(array_filter($frame->graph->definitions, static fn ($instruction1): bool => $instruction1->operation === $operation1))[0] ?? null;
126 self::assertNotNull($instruction1);
127 self::assertTrue(BodyIndex::writes($instruction1));
128 $operation2 = 'increment';
129 $instruction2 = array_values(array_filter($frame->graph->definitions, static fn ($instruction2): bool => $instruction2->operation === $operation2))[0] ?? null;
130 self::assertNotNull($instruction2);
131 self::assertTrue(BodyIndex::writes($instruction2));
132 $operation3 = 'unset';
133 $instruction3 = array_values(array_filter($frame->graph->definitions, static fn ($instruction3): bool => $instruction3->operation === $operation3))[0] ?? null;
134 self::assertNotNull($instruction3);
135 self::assertTrue(BodyIndex::writes($instruction3));
136 $operation4 = 'local';
137 $instruction4 = array_values(array_filter($frame->graph->definitions, static fn ($instruction4): bool => $instruction4->operation === $operation4))[0] ?? null;
138 self::assertNotNull($instruction4);
139 self::assertFalse(BodyIndex::writes($instruction4));
140 }
141 public function testRootFindsThePropertyBehindAnElementMutation(): void
142 {
143 $source = 'class Box{public $a=[1];function target(){$this->a[0]+=2;}}';
144 $configuration = new DeriverConfig();
145 $budget = new Budget();
146 $contributions = new Contributions();
147 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
148 $providers = new ProviderExpansion(new Sources([new Source('candidate.php', '<?php ' . $source)]), array_map(Term::fromNative(...), $configuration->environment), $contributions);
149 $contributions1 = $providers->contributions;
150 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
151 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
152 $snapshotId = 'test';
153 $input = $providers->input;
154 $parser = new Parser(new ParserConfig($target->version));
155 $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));
156 $program = new Index($snapshotId, $trees);
157 $evaluator = new Evaluator(new Context(new DefinitionIndex($program), $settings, $evaluators, $budget));
158 $symbol = 'Box::target';
159 $graph = $evaluator->context->index->graph($symbol);
160 self::assertNotNull($graph);
161 $frame = new Frame($graph, 'source:' . $symbol);
162 $operation = 'compound';
163 $instruction = array_values(array_filter($frame->graph->definitions, static fn ($instruction): bool => $instruction->operation === $operation))[0] ?? null;
164 self::assertNotNull($instruction);
165 self::assertSame('field-address', $frame->graph->root($instruction)?->operation);
166 }
167}
168