packages/deriver/tests/Unit/Deriver/Evaluator/Execution/Control/ExceptionMatchTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Execution\Control;
6
7use Deriver\Deriver\Contributions;
8use Deriver\Deriver\DeriverConfig;
9use Deriver\Deriver\Evaluator\Call\Evaluators;
10use Deriver\Deriver\Evaluator\Call\Extensions;
11use Deriver\Deriver\Evaluator\Call\Registrations;
12use Deriver\Deriver\Evaluator\Call\StateRegistry;
13use Deriver\Deriver\Evaluator\EvaluatorConfig;
14use Deriver\Deriver\Evaluator\Evidence\Frontier;
15use Deriver\Deriver\Evaluator\Execution\Context;
16use Deriver\Deriver\Evaluator\Execution\Control\ExceptionMatch;
17use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
18use Deriver\Deriver\Evaluator\Request\Budget;
19use Deriver\Deriver\Evaluator\Request\QueryScope;
20use Deriver\Deriver\Evaluator\Request\ReturnQuery;
21use Deriver\Deriver\Evaluator\TargetProfile;
22use Deriver\Deriver\Evaluator\Term;
23use Deriver\Deriver\Evaluator\TermKind;
24use Deriver\Deriver\Execution\QueryExecution;
25use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
26use Deriver\Deriver\Lowerer\Declaration\CallableSource;
27use Deriver\Deriver\Lowerer\Declaration\ConstantSignatures;
28use Deriver\Deriver\Lowerer\Declaration\Index;
29use Deriver\Deriver\Lowerer\Declaration\Indexer;
30use Deriver\Deriver\Lowerer\Declaration\Metadata\ClassDeclaration;
31use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
32use Deriver\Deriver\Lowerer\Hierarchy\Builtins;
33use Deriver\Deriver\Lowerer\Hierarchy\Dispatch;
34use Deriver\Deriver\MicroEvaluatorConversion;
35use Deriver\Deriver\Parser\Node\File;
36use Deriver\Deriver\Parser\Parser;
37use Deriver\Deriver\Parser\ParserConfig;
38use Deriver\Deriver\Parser\TargetSyntax;
39use Deriver\Deriver\Providers\ProviderExpansion;
40use Deriver\Deriver\Resolver\AssignmentPatterns;
41use Deriver\Deriver\Resolver\ClassScope;
42use Deriver\Deriver\Resolver\ExpressionValidation;
43use Deriver\Deriver\Resolver\MagicContext;
44use Deriver\Deriver\Resolver\Resolver;
45use Deriver\Deriver\SnapshotIdentity;
46use Deriver\Deriver\Sources\LineMap;
47use Deriver\Deriver\Sources\Path;
48use Deriver\Deriver\Sources\Source;
49use Deriver\Deriver\Sources\Sources;
50use Deriver\Exception\ParserException;
51use Deriver\Exception\ResolutionException;
52use PHPUnit\Framework\Attributes\CoversClass;
53use PHPUnit\Framework\Attributes\DataProvider;
54use PHPUnit\Framework\Attributes\Small;
55use PHPUnit\Framework\Attributes\UsesClass;
56use PHPUnit\Framework\TestCase;
57
58#[UsesClass(Resolver::class)]
59#[UsesClass(ExpressionValidation::class)]
60#[UsesClass(ResolutionException::class)]
61#[UsesClass(ParserConfig::class)]
62#[UsesClass(ParserException::class)]
63#[CoversClass(ExceptionMatch::class)]
64#[UsesClass(CallableIdentity::class)]
65#[UsesClass(ClassDeclaration::class)]
66#[UsesClass(Builtins::class)]
67#[UsesClass(Dispatch::class)]
68#[UsesClass(Context::class)]
69#[UsesClass(Extensions::class)]
70#[UsesClass(Evaluators::class)]
71#[UsesClass(StateRegistry::class)]
72#[UsesClass(DeriverConfig::class)]
73#[UsesClass(Sources::class)]
74#[UsesClass(Path::class)]
75#[UsesClass(Source::class)]
76#[UsesClass(TargetProfile::class)]
77#[UsesClass(Budget::class)]
78#[UsesClass(QueryScope::class)]
79#[UsesClass(ReturnQuery::class)]
80#[UsesClass(Parser::class)]
81#[UsesClass(File::class)]
82#[UsesClass(ConstantSignatures::class)]
83#[UsesClass(CallableSource::class)]
84#[UsesClass(Indexer::class)]
85#[UsesClass(Index::class)]
86#[UsesClass(Composition::class)]
87#[UsesClass(LineMap::class)]
88#[UsesClass(MagicContext::class)]
89#[UsesClass(AssignmentPatterns::class)]
90#[UsesClass(ClassScope::class)]
91#[UsesClass(TargetSyntax::class)]
92#[UsesClass(Term::class)]
93#[Small]
94final class ExceptionMatchTest extends TestCase
95{
96 /**
97 * @param string $bound Input upper bound
98 * @param list<string> $catches Catch union
99 * @param bool $matched Whether the catch is feasible
100 * @param bool $remaining Whether an uncaught subset is feasible
101 */
102 #[DataProvider('providerBounds')]
103 public function testPartitionRetainsExactlyTheFeasibleSubsets(string $bound, array $catches, bool $matched, bool $remaining): void
104 {
105 $source = '<?php interface Tagged{}final class Box{}class ExternalProblem extends Missing{}class Problem extends RuntimeException implements Tagged{}';
106 $budget = new Budget();
107 $configuration = new DeriverConfig();
108 $contributions = new Contributions();
109 $input = new Sources([new Source('fixture.php', $source)]);
110 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
111 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
112 $contributions1 = $providers->contributions;
113 $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);
114 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
115 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
116 $snapshotId = $identity->id;
117 $input1 = $providers->input;
118 $parser = new Parser(new ParserConfig($target->version));
119 $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($input1, false));
120 $program1 = new Index($snapshotId, $trees);
121 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
122 $snapshotId1 = 'test';
123 $input2 = new Sources([new Source('fixture.php', $source)]);
124 $profile = $execution->configuration->target;
125 $parser1 = new Parser(new ParserConfig($profile->version));
126 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
127 $program = new Index($snapshotId1, $trees1);
128 $types = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
129 [$caught, $rest] = $types->partition(Term::parameter('input', $bound), $catches);
130 self::assertSame($matched, $caught !== null);
131 self::assertSame($remaining, $rest !== null);
132 }
133 /**
134 * @return iterable<string, array{string, list<string>, bool, bool}>
135 */
136 public static function providerBounds(): iterable
137 {
138 yield 'definite root' => ['RuntimeException', ['Exception'], true, false];
139 yield 'disjoint roots' => ['Exception', ['Error'], false, true];
140 yield 'possible subclass' => ['Exception', ['RuntimeException'], true, true];
141 yield 'all throwable roots' => ['Throwable', ['Exception', 'Error'], true, false];
142 yield 'one throwable root' => ['Throwable', ['Error'], true, true];
143 yield 'union fully caught' => ['RuntimeException|TypeError', ['Exception', 'Error'], true, false];
144 yield 'mixed invalid subset' => ['mixed', ['Throwable'], true, true];
145 yield 'object invalid subset' => ['object', ['Throwable'], true, true];
146 yield 'int invalid' => ['int', ['Throwable'], false, true];
147 yield 'primitive union invalid' => ['null|array|string', ['Throwable'], false, true];
148 yield 'closure bound invalid' => ['Closure', ['Throwable'], false, true];
149 yield 'ordinary class invalid' => ['Box', ['Throwable'], false, true];
150 yield 'implemented interface' => ['Problem', ['Tagged'], true, false];
151 yield 'interface may be throwable' => ['Tagged', ['Throwable'], true, true];
152 yield 'intersection root' => ['Tagged&RuntimeException', ['Exception'], true, false];
153 yield 'intersection subtype' => ['Tagged&Exception', ['RuntimeException'], true, true];
154 yield 'intersection disjoint' => ['Tagged&Exception', ['Error'], false, true];
155 yield 'dnf partly caught' => ['(Tagged&Exception)|Error', ['Tagged'], true, true];
156 yield 'missing parent' => ['ExternalProblem', ['Throwable'], true, true];
157 yield 'unknown hierarchy' => ['VendorProblem', ['Exception'], true, true];
158 yield 'invokable exception possible' => ['callable', ['Throwable'], true, true];
159 }
160 public function testPartitionPreservesExclusionsAcrossRepeatedCatchClauses(): void
161 {
162 $source = '<?php function target(){return 1;}';
163 $budget = new Budget();
164 $configuration = new DeriverConfig();
165 $contributions = new Contributions();
166 $input1 = new Sources([new Source('fixture.php', $source)]);
167 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
168 $providers = new ProviderExpansion($input1, array_map(Term::fromNative(...), $configuration->environment), $contributions);
169 $contributions1 = $providers->contributions;
170 $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);
171 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
172 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
173 $snapshotId = $identity->id;
174 $input2 = $providers->input;
175 $parser = new Parser(new ParserConfig($target->version));
176 $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($input2, false));
177 $program1 = new Index($snapshotId, $trees);
178 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
179 $snapshotId1 = 'test';
180 $input3 = new Sources([new Source('fixture.php', $source)]);
181 $profile = $execution->configuration->target;
182 $parser1 = new Parser(new ParserConfig($profile->version));
183 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input3, false));
184 $program = new Index($snapshotId1, $trees1);
185 $types = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
186 $input = Term::parameter('input', 'Exception');
187 [$caught, $remaining] = $types->partition($input, ['RuntimeException']);
188 self::assertNotNull($caught);
189 self::assertSame('RuntimeException', $caught->attributes['type']);
190 self::assertSame($input, $caught->operands[0]);
191 self::assertNotNull($remaining);
192 [$shadowed, $rest] = $types->partition($remaining, ['OverflowException']);
193 self::assertNull($shadowed);
194 self::assertSame($remaining, $rest);
195 self::assertSame([$remaining, null], $types->partition($remaining, ['Exception']));
196 }
197 /**
198 * @param Term $value Evaluated throw operand
199 * @param bool $valid Whether the exact value is a Throwable
200 */
201 #[DataProvider('providerExactValues')]
202 public function testExactRecognizesRuntimeValuesAndClassIdentity(Term $value, bool $valid): void
203 {
204 $source = '<?php function target(){return 1;}';
205 $budget = new Budget();
206 $configuration = new DeriverConfig();
207 $contributions = new Contributions();
208 $input = new Sources([new Source('fixture.php', $source)]);
209 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
210 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
211 $contributions1 = $providers->contributions;
212 $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);
213 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
214 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
215 $snapshotId = $identity->id;
216 $input1 = $providers->input;
217 $parser = new Parser(new ParserConfig($target->version));
218 $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($input1, false));
219 $program1 = new Index($snapshotId, $trees);
220 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
221 $snapshotId1 = 'test';
222 $input2 = new Sources([new Source('fixture.php', $source)]);
223 $profile = $execution->configuration->target;
224 $parser1 = new Parser(new ParserConfig($profile->version));
225 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
226 $program = new Index($snapshotId1, $trees1);
227 $types = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
228 self::assertSame($valid ? [$value, null] : [null, $value], $types->partition($value, ['Throwable']));
229 }
230 /**
231 * @return iterable<string, array{Term, bool}>
232 */
233 public static function providerExactValues(): iterable
234 {
235 yield 'integer' => [Term::constant(5), false];
236 yield 'null' => [Term::constant(null), false];
237 yield 'array' => [Term::array([]), false];
238 yield 'closure' => [new Term(TermKind::Closure, 'body'), false];
239 yield 'enum' => [new Term(TermKind::Enum, 'Mode::Ready'), false];
240 yield 'plain object' => [new Term(TermKind::Object, 'box', attributes: ['class' => 'stdClass']), false];
241 yield 'native error' => [new Term(TermKind::Throwable, 'TypeError'), true];
242 yield 'allocated exception' => [new Term(TermKind::Object, 'exception', attributes: ['class' => 'RuntimeException']), true];
243 yield 'uncertain throwable' => [new Term(TermKind::Throwable, 'Throwable', attributes: ['uncertain' => true]), true];
244 }
245 public function testRefineDeduplicatesBounds(): void
246 {
247 $input = new Term(TermKind::Parameter, 'item', attributes: ['type' => 'Throwable']);
248 $source = '<?php function target(){return 1;}';
249 $budget = new Budget();
250 $configuration = new DeriverConfig();
251 $contributions = new Contributions();
252 $input1 = new Sources([new Source('fixture.php', $source)]);
253 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
254 $providers = new ProviderExpansion($input1, array_map(Term::fromNative(...), $configuration->environment), $contributions);
255 $contributions1 = $providers->contributions;
256 $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);
257 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
258 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
259 $snapshotId = $identity->id;
260 $input2 = $providers->input;
261 $parser = new Parser(new ParserConfig($target->version));
262 $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($input2, false));
263 $program1 = new Index($snapshotId, $trees);
264 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
265 $snapshotId1 = 'test';
266 $input3 = new Sources([new Source('fixture.php', $source)]);
267 $profile = $execution->configuration->target;
268 $parser1 = new Parser(new ParserConfig($profile->version));
269 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input3, false));
270 $program = new Index($snapshotId1, $trees1);
271 $refined = (new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program))->refine($input, ['Error', 'Error'], ['', 'TypeError', 'TypeError']);
272 self::assertSame(['type' => 'Error', 'excluded-types' => 'TypeError'], $refined->attributes);
273 self::assertSame($input, $refined->operands[0]);
274 }
275 public function testArmsExpandsThrowableRootsAndPreservesIntersections(): void
276 {
277 $source = '<?php function target(){return 1;}';
278 $budget = new Budget();
279 $configuration = new DeriverConfig();
280 $contributions = new Contributions();
281 $input = new Sources([new Source('fixture.php', $source)]);
282 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
283 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
284 $contributions1 = $providers->contributions;
285 $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);
286 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
287 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
288 $snapshotId = $identity->id;
289 $input1 = $providers->input;
290 $parser = new Parser(new ParserConfig($target->version));
291 $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($input1, false));
292 $program1 = new Index($snapshotId, $trees);
293 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
294 $snapshotId1 = 'test';
295 $input2 = new Sources([new Source('fixture.php', $source)]);
296 $profile = $execution->configuration->target;
297 $parser1 = new Parser(new ParserConfig($profile->version));
298 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
299 $program = new Index($snapshotId1, $trees1);
300 $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
301 self::assertSame(['Exception', 'Error', 'Tagged&Exception', 'null'], $match->arms('throwable|(Tagged&Exception)|null'));
302 }
303 public function testIntersectionRetainsIndependentInterfaceBounds(): void
304 {
305 $source = '<?php interface Tagged{}';
306 $budget = new Budget();
307 $configuration = new DeriverConfig();
308 $contributions = new Contributions();
309 $input = new Sources([new Source('fixture.php', $source)]);
310 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
311 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
312 $contributions1 = $providers->contributions;
313 $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);
314 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
315 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
316 $snapshotId = $identity->id;
317 $input1 = $providers->input;
318 $parser = new Parser(new ParserConfig($target->version));
319 $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($input1, false));
320 $program1 = new Index($snapshotId, $trees);
321 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
322 $snapshotId1 = 'test';
323 $input2 = new Sources([new Source('fixture.php', $source)]);
324 $profile = $execution->configuration->target;
325 $parser1 = new Parser(new ParserConfig($profile->version));
326 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
327 $program = new Index($snapshotId1, $trees1);
328 $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
329 self::assertSame('Tagged&RuntimeException', $match->intersection('Tagged&Exception', 'RuntimeException'));
330 self::assertNull($match->intersection('RuntimeException', 'LogicException'));
331 self::assertSame('RuntimeException', $match->intersection('mixed', 'RuntimeException'));
332 }
333 public function testImpliesDistinguishesNecessaryAndPossibleSubclassRelations(): void
334 {
335 $source = '<?php function target(){return 1;}';
336 $budget = new Budget();
337 $configuration = new DeriverConfig();
338 $contributions = new Contributions();
339 $input = new Sources([new Source('fixture.php', $source)]);
340 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
341 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
342 $contributions1 = $providers->contributions;
343 $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);
344 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
345 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
346 $snapshotId = $identity->id;
347 $input1 = $providers->input;
348 $parser = new Parser(new ParserConfig($target->version));
349 $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($input1, false));
350 $program1 = new Index($snapshotId, $trees);
351 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
352 $snapshotId1 = 'test';
353 $input2 = new Sources([new Source('fixture.php', $source)]);
354 $profile = $execution->configuration->target;
355 $parser1 = new Parser(new ParserConfig($profile->version));
356 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
357 $program = new Index($snapshotId1, $trees1);
358 $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
359 self::assertTrue($match->implies('Tagged&RuntimeException', 'Throwable'));
360 self::assertFalse($match->implies('Exception', 'RuntimeException'));
361 }
362 public function testOverlapsRetainsUnknownInterfacesAndRejectsSiblingClasses(): void
363 {
364 $source = '<?php interface Tagged{}';
365 $budget = new Budget();
366 $configuration = new DeriverConfig();
367 $contributions = new Contributions();
368 $input = new Sources([new Source('fixture.php', $source)]);
369 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
370 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
371 $contributions1 = $providers->contributions;
372 $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);
373 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
374 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
375 $snapshotId = $identity->id;
376 $input1 = $providers->input;
377 $parser = new Parser(new ParserConfig($target->version));
378 $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($input1, false));
379 $program1 = new Index($snapshotId, $trees);
380 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
381 $snapshotId1 = 'test';
382 $input2 = new Sources([new Source('fixture.php', $source)]);
383 $profile = $execution->configuration->target;
384 $parser1 = new Parser(new ParserConfig($profile->version));
385 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
386 $program = new Index($snapshotId1, $trees1);
387 $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
388 self::assertTrue($match->overlaps('Tagged', 'Exception'));
389 self::assertTrue($match->overlaps('Exception', 'RuntimeException'));
390 self::assertFalse($match->overlaps('RuntimeException', 'LogicException'));
391 self::assertFalse($match->overlaps('int', 'Exception'));
392 }
393 public function testConcreteClassSeparatesSourceAndNativeClassesFromInterfaces(): void
394 {
395 $source = '<?php interface Tagged{}class Box{}';
396 $budget = new Budget();
397 $configuration = new DeriverConfig();
398 $contributions = new Contributions();
399 $input = new Sources([new Source('fixture.php', $source)]);
400 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
401 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
402 $contributions1 = $providers->contributions;
403 $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);
404 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
405 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
406 $snapshotId = $identity->id;
407 $input1 = $providers->input;
408 $parser = new Parser(new ParserConfig($target->version));
409 $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($input1, false));
410 $program1 = new Index($snapshotId, $trees);
411 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
412 $snapshotId1 = 'test';
413 $input2 = new Sources([new Source('fixture.php', $source)]);
414 $profile = $execution->configuration->target;
415 $parser1 = new Parser(new ParserConfig($profile->version));
416 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
417 $program = new Index($snapshotId1, $trees1);
418 $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
419 self::assertTrue($match->concreteClass('Box'));
420 self::assertTrue($match->concreteClass('Exception'));
421 self::assertFalse($match->concreteClass('Throwable'));
422 self::assertFalse($match->concreteClass('Tagged'));
423 self::assertFalse($match->concreteClass('Missing'));
424 }
425 public function testExcludedRemovesAllSubclassesOfAnEarlierCatch(): void
426 {
427 $source = '<?php function target(){return 1;}';
428 $budget = new Budget();
429 $configuration = new DeriverConfig();
430 $contributions = new Contributions();
431 $input = new Sources([new Source('fixture.php', $source)]);
432 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
433 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
434 $contributions1 = $providers->contributions;
435 $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);
436 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
437 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
438 $snapshotId = $identity->id;
439 $input1 = $providers->input;
440 $parser = new Parser(new ParserConfig($target->version));
441 $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($input1, false));
442 $program1 = new Index($snapshotId, $trees);
443 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
444 $snapshotId1 = 'test';
445 $input2 = new Sources([new Source('fixture.php', $source)]);
446 $profile = $execution->configuration->target;
447 $parser1 = new Parser(new ParserConfig($profile->version));
448 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
449 $program = new Index($snapshotId1, $trees1);
450 $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
451 self::assertTrue($match->excluded('OverflowException', ['', 'RuntimeException']));
452 self::assertFalse($match->excluded('LogicException', ['', 'RuntimeException']));
453 }
454 public function testPartitionRetainsUnknownParentsForAllocatedObjects(): void
455 {
456 $source = '<?php class Problem extends Missing{}';
457 $budget = new Budget();
458 $configuration = new DeriverConfig();
459 $contributions = new Contributions();
460 $input = new Sources([new Source('fixture.php', $source)]);
461 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
462 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
463 $contributions1 = $providers->contributions;
464 $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);
465 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
466 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
467 $snapshotId = $identity->id;
468 $input1 = $providers->input;
469 $parser = new Parser(new ParserConfig($target->version));
470 $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($input1, false));
471 $program1 = new Index($snapshotId, $trees);
472 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
473 $snapshotId1 = 'test';
474 $input2 = new Sources([new Source('fixture.php', $source)]);
475 $profile = $execution->configuration->target;
476 $parser1 = new Parser(new ParserConfig($profile->version));
477 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
478 $program = new Index($snapshotId1, $trees1);
479 $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
480 [$caught, $remaining] = $match->partition(new Term(TermKind::Object, 'problem', attributes: ['class' => 'Problem']), ['Throwable']);
481 self::assertNotNull($caught);
482 self::assertNotNull($remaining);
483 self::assertSame('Problem&Exception|Problem&Error', $caught->attributes['type']);
484 }
485}
486