packages/deriver/tests/Unit/Deriver/Lowerer/Declaration/IndexerTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Lowerer\Declaration;
6
7use Deriver\Deriver;
8use Deriver\Deriver\Candidate\ResultRef;
9use Deriver\Deriver\Candidate\Snapshot;
10use Deriver\Deriver\Candidate\Statistics;
11use Deriver\Deriver\Contributions;
12use Deriver\Deriver\DeriverConfig;
13use Deriver\Deriver\Evaluator\Call\Evaluators;
14use Deriver\Deriver\Evaluator\Call\Extensions;
15use Deriver\Deriver\Evaluator\Call\Registrations;
16use Deriver\Deriver\Evaluator\Call\StateRegistry;
17use Deriver\Deriver\Evaluator\EvaluatorConfig;
18use Deriver\Deriver\Evaluator\Evidence\Frontier;
19use Deriver\Deriver\Evaluator\Execution\Call\Allocation;
20use Deriver\Deriver\Evaluator\Execution\Call\ArgumentBinding;
21use Deriver\Deriver\Evaluator\Execution\Call\ArgumentOrder;
22use Deriver\Deriver\Evaluator\Execution\Call\CallExecutor;
23use Deriver\Deriver\Evaluator\Execution\Call\CallResolution;
24use Deriver\Deriver\Evaluator\Execution\Call\Creation\Access;
25use Deriver\Deriver\Evaluator\Execution\Call\Member\Invocation;
26use Deriver\Deriver\Evaluator\Execution\Call\ParameterBinding;
27use Deriver\Deriver\Evaluator\Execution\Call\PassedArgument;
28use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Arguments;
29use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Creation;
30use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Modes;
31use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Resolution;
32use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Target;
33use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Transfer;
34use Deriver\Deriver\Evaluator\Execution\Call\TypeBinding;
35use Deriver\Deriver\Evaluator\Execution\Call\TypeCheck;
36use Deriver\Deriver\Evaluator\Execution\Completion\Alternative;
37use Deriver\Deriver\Evaluator\Execution\Completion\Assessment;
38use Deriver\Deriver\Evaluator\Execution\Completion\Completion;
39use Deriver\Deriver\Evaluator\Execution\Completion\StorageSnapshot;
40use Deriver\Deriver\Evaluator\Execution\Context;
41use Deriver\Deriver\Evaluator\Execution\Control\ObservationLimit;
42use Deriver\Deriver\Evaluator\Execution\Control\StateJoin;
43use Deriver\Deriver\Evaluator\Execution\Control\Unwinding;
44use Deriver\Deriver\Evaluator\Execution\Demand\Discovery;
45use Deriver\Deriver\Evaluator\Execution\Dependencies;
46use Deriver\Deriver\Evaluator\Execution\Evaluator;
47use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
48use Deriver\Deriver\Evaluator\Execution\InstructionTransfer;
49use Deriver\Deriver\Evaluator\Execution\Memory\Location;
50use Deriver\Deriver\Evaluator\Execution\Memory\Materialization;
51use Deriver\Deriver\Evaluator\Execution\Memory\Memory;
52use Deriver\Deriver\Evaluator\Execution\Memory\ReferenceConstraint;
53use Deriver\Deriver\Evaluator\Execution\Memory\Slots\SlotReference;
54use Deriver\Deriver\Evaluator\Execution\Memory\Slots\StateStorage;
55use Deriver\Deriver\Evaluator\Execution\Memory\StorageCapture;
56use Deriver\Deriver\Evaluator\Execution\ObservationCollector;
57use Deriver\Deriver\Evaluator\Execution\Operation\Conversions;
58use Deriver\Deriver\Evaluator\Execution\Operation\ScalarErrors;
59use Deriver\Deriver\Evaluator\Execution\State;
60use Deriver\Deriver\Evaluator\Execution\Summary\Evaluation;
61use Deriver\Deriver\Evaluator\Execution\Summary\Isolation;
62use Deriver\Deriver\Evaluator\Execution\Trace\Derivation;
63use Deriver\Deriver\Evaluator\Execution\Transfer\MemoryStep;
64use Deriver\Deriver\Evaluator\Execution\Transfer\ObjectAccess;
65use Deriver\Deriver\Evaluator\Execution\Transfer\PropertyAccessCheck;
66use Deriver\Deriver\Evaluator\Execution\Transfer\PropertyLookup;
67use Deriver\Deriver\Evaluator\Execution\Transfer\PropertySlot;
68use Deriver\Deriver\Evaluator\Execution\Transfer\PropertyTransfer;
69use Deriver\Deriver\Evaluator\Execution\Transfer\PureStep;
70use Deriver\Deriver\Evaluator\Execution\Transfer\ReferenceAssignment;
71use Deriver\Deriver\Evaluator\Request\Budget;
72use Deriver\Deriver\Evaluator\Request\EntryPoint;
73use Deriver\Deriver\Evaluator\Request\QueryScope;
74use Deriver\Deriver\Evaluator\Request\ReturnQuery;
75use Deriver\Deriver\Evaluator\Scalar\Arrays;
76use Deriver\Deriver\Evaluator\Scalar\Identity;
77use Deriver\Deriver\Evaluator\Scalar\Operations;
78use Deriver\Deriver\Evaluator\TargetProfile;
79use Deriver\Deriver\Evaluator\Term as EvaluatorTerm;
80use Deriver\Deriver\Execution\DerivationResult;
81use Deriver\Deriver\Execution\QueryExecution;
82use Deriver\Deriver\Execution\ResultAssessment;
83use Deriver\Deriver\Identity\QueryEncoding;
84use Deriver\Deriver\Identity\ValueGraph;
85use Deriver\Deriver\Lowerer\CallLowering;
86use Deriver\Deriver\Lowerer\Control\DestructuringLowering;
87use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
88use Deriver\Deriver\Lowerer\Declaration\CallableSource;
89use Deriver\Deriver\Lowerer\Declaration\ConstantSignatures;
90use Deriver\Deriver\Lowerer\Declaration\Index;
91use Deriver\Deriver\Lowerer\Declaration\Indexer;
92use Deriver\Deriver\Lowerer\Declaration\Metadata\ClassConstant;
93use Deriver\Deriver\Lowerer\Declaration\Metadata\ClassDeclaration;
94use Deriver\Deriver\Lowerer\Declaration\Metadata\PropertyDeclaration;
95use Deriver\Deriver\Lowerer\Declaration\Metadata\Visibility;
96use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
97use Deriver\Deriver\Lowerer\Declaration\Traits\Members;
98use Deriver\Deriver\Lowerer\EffectInspection;
99use Deriver\Deriver\Lowerer\ExpressionLowering;
100use Deriver\Deriver\Lowerer\Graph\Argument;
101use Deriver\Deriver\Lowerer\Graph\BasicBlock;
102use Deriver\Deriver\Lowerer\Graph\CallableGraph;
103use Deriver\Deriver\Lowerer\Graph\Instruction;
104use Deriver\Deriver\Lowerer\Graph\Literal as Term;
105use Deriver\Deriver\Lowerer\Graph\LiteralKind;
106use Deriver\Deriver\Lowerer\Graph\Parameter;
107use Deriver\Deriver\Lowerer\Graph\Terminator;
108use Deriver\Deriver\Lowerer\Hierarchy\Access as HierarchyAccess;
109use Deriver\Deriver\Lowerer\Hierarchy\Dispatch;
110use Deriver\Deriver\Lowerer\InstructionEmitter;
111use Deriver\Deriver\Lowerer\Lowerer;
112use Deriver\Deriver\Lowerer\LoweringScope;
113use Deriver\Deriver\Lowerer\StatementLowering;
114use Deriver\Deriver\MicroEvaluatorConversion;
115use Deriver\Deriver\Parser\Node\File;
116use Deriver\Deriver\Parser\Parser;
117use Deriver\Deriver\Parser\ParserConfig;
118use Deriver\Deriver\Parser\TargetSyntax;
119use Deriver\Deriver\Providers\ProviderExpansion;
120use Deriver\Deriver\Resolver\AssignmentPatterns;
121use Deriver\Deriver\Resolver\ClassScope;
122use Deriver\Deriver\Resolver\ExpressionValidation;
123use Deriver\Deriver\Resolver\MagicContext;
124use Deriver\Deriver\Resolver\Resolver;
125use Deriver\Deriver\Selection\QueryValidation;
126use Deriver\Deriver\SnapshotIdentity;
127use Deriver\Deriver\Sources\LineMap;
128use Deriver\Deriver\Sources\Path;
129use Deriver\Deriver\Sources\Source;
130use Deriver\Deriver\Sources\SourceRef;
131use Deriver\Deriver\Sources\Sources;
132use Deriver\Exception\ParserException;
133use Deriver\Exception\ResolutionException;
134use PhpParser\Node\DeclareItem;
135use PhpParser\Node\Scalar;
136use PhpParser\Node\Scalar\Int_;
137use PhpParser\Node\Scalar\String_;
138use PhpParser\Node\Stmt;
139use PhpParser\Node\Stmt\Class_;
140use PhpParser\Node\Stmt\Const_;
141use PhpParser\Node\Stmt\Declare_;
142use PhpParser\Node\Stmt\Function_;
143use PhpParser\Node\Stmt\Nop;
144use PhpParser\ParserFactory;
145use PHPUnit\Framework\Attributes\CoversClass;
146use PHPUnit\Framework\Attributes\DataProvider;
147use PHPUnit\Framework\Attributes\Small;
148use PHPUnit\Framework\Attributes\UsesClass;
149use PHPUnit\Framework\TestCase;
150
151#[UsesClass(Resolver::class)]
152#[UsesClass(ExpressionValidation::class)]
153#[UsesClass(ResolutionException::class)]
154#[UsesClass(ParserConfig::class)]
155#[UsesClass(ParserException::class)]
156#[CoversClass(Indexer::class)]
157#[UsesClass(QueryExecution::class)]
158#[UsesClass(QueryValidation::class)]
159#[UsesClass(ResultAssessment::class)]
160#[UsesClass(Deriver::class)]
161#[UsesClass(Argument::class)]
162#[UsesClass(BasicBlock::class)]
163#[UsesClass(CallableGraph::class)]
164#[UsesClass(CallableIdentity::class)]
165#[UsesClass(ClassConstant::class)]
166#[UsesClass(ClassDeclaration::class)]
167#[UsesClass(Instruction::class)]
168#[UsesClass(Parameter::class)]
169#[UsesClass(PropertyDeclaration::class)]
170#[UsesClass(Terminator::class)]
171#[UsesClass(Allocation::class)]
172#[UsesClass(ArgumentBinding::class)]
173#[UsesClass(ArgumentOrder::class)]
174#[UsesClass(CallExecutor::class)]
175#[UsesClass(CallResolution::class)]
176#[UsesClass(Access::class)]
177#[UsesClass(Dispatch::class)]
178#[UsesClass(HierarchyAccess::class)]
179#[UsesClass(Invocation::class)]
180#[UsesClass(ParameterBinding::class)]
181#[UsesClass(PassedArgument::class)]
182#[UsesClass(Arguments::class)]
183#[UsesClass(Creation::class)]
184#[UsesClass(Modes::class)]
185#[UsesClass(Resolution::class)]
186#[UsesClass(Target::class)]
187#[UsesClass(Transfer::class)]
188#[UsesClass(TypeBinding::class)]
189#[UsesClass(TypeCheck::class)]
190#[UsesClass(Completion::class)]
191#[UsesClass(Context::class)]
192#[UsesClass(ObservationLimit::class)]
193#[UsesClass(StateJoin::class)]
194#[UsesClass(Unwinding::class)]
195#[UsesClass(Discovery::class)]
196#[UsesClass(Dependencies::class)]
197#[UsesClass(InstructionTransfer::class)]
198#[UsesClass(Evaluator::class)]
199#[UsesClass(SlotReference::class)]
200#[UsesClass(StateStorage::class)]
201#[UsesClass(ObservationCollector::class)]
202#[UsesClass(Conversions::class)]
203#[UsesClass(ScalarErrors::class)]
204#[UsesClass(State::class)]
205#[UsesClass(Evaluation::class)]
206#[UsesClass(Isolation::class)]
207#[UsesClass(MemoryStep::class)]
208#[UsesClass(ObjectAccess::class)]
209#[UsesClass(PropertyAccessCheck::class)]
210#[UsesClass(PropertyLookup::class)]
211#[UsesClass(PropertySlot::class)]
212#[UsesClass(PropertyTransfer::class)]
213#[UsesClass(PureStep::class)]
214#[UsesClass(ReferenceAssignment::class)]
215#[UsesClass(Location::class)]
216#[UsesClass(Materialization::class)]
217#[UsesClass(Memory::class)]
218#[UsesClass(ReferenceConstraint::class)]
219#[UsesClass(StorageCapture::class)]
220#[UsesClass(Extensions::class)]
221#[UsesClass(ProviderExpansion::class)]
222#[UsesClass(Evaluators::class)]
223#[UsesClass(StateRegistry::class)]
224#[UsesClass(DeriverConfig::class)]
225#[UsesClass(EntryPoint::class)]
226#[UsesClass(Sources::class)]
227#[UsesClass(Path::class)]
228#[UsesClass(Snapshot::class)]
229#[UsesClass(Source::class)]
230#[UsesClass(TargetProfile::class)]
231#[UsesClass(Budget::class)]
232#[UsesClass(QueryScope::class)]
233#[UsesClass(ReturnQuery::class)]
234#[UsesClass(ResultRef::class)]
235#[UsesClass(SourceRef::class)]
236#[UsesClass(Alternative::class)]
237#[UsesClass(Assessment::class)]
238#[UsesClass(Derivation::class)]
239#[UsesClass(DerivationResult::class)]
240#[UsesClass(Frontier::class)]
241#[UsesClass(QueryEncoding::class)]
242#[UsesClass(ValueGraph::class)]
243#[UsesClass(Statistics::class)]
244#[UsesClass(StorageSnapshot::class)]
245#[UsesClass(Parser::class)]
246#[UsesClass(File::class)]
247#[UsesClass(CallLowering::class)]
248#[UsesClass(Lowerer::class)]
249#[UsesClass(DestructuringLowering::class)]
250#[UsesClass(EffectInspection::class)]
251#[UsesClass(ExpressionLowering::class)]
252#[UsesClass(InstructionEmitter::class)]
253#[UsesClass(LoweringScope::class)]
254#[UsesClass(StatementLowering::class)]
255#[UsesClass(ConstantSignatures::class)]
256#[UsesClass(CallableSource::class)]
257#[UsesClass(Index::class)]
258#[UsesClass(Composition::class)]
259#[UsesClass(Members::class)]
260#[UsesClass(LineMap::class)]
261#[UsesClass(MagicContext::class)]
262#[UsesClass(AssignmentPatterns::class)]
263#[UsesClass(ClassScope::class)]
264#[UsesClass(TargetSyntax::class)]
265#[UsesClass(Arrays::class)]
266#[UsesClass(Identity::class)]
267#[UsesClass(Operations::class)]
268#[UsesClass(Term::class)]
269#[Small]
270final class IndexerTest extends TestCase
271{
272 public function testStrictPreservesTheSemanticContract(): void
273 {
274 $source = '<?php class Box{function __construct(public string $value){}}function target(){return (new Box("yes"))->value;}';
275 $symbol = 'target';
276 $input = new Sources([new Source('fixture.php', $source)]);
277 $configuration = new DeriverConfig();
278 $contributions = new Contributions();
279 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
280 $providers = new ProviderExpansion($input, array_map(EvaluatorTerm::fromNative(...), $configuration->environment), $contributions);
281 $contributions1 = $providers->contributions;
282 $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);
283 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
284 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
285 $snapshotId = $identity->id;
286 $input1 = $providers->input;
287 $parser = new Parser(new ParserConfig($target->version));
288 $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));
289 $program = new Index($snapshotId, $trees);
290 $result = (new QueryExecution($program, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program->diagnostics()))))->derive(new ReturnQuery($symbol));
291 self::assertSame('yes', $result->normalOutcomes[0]->values['return']->native());
292 self::assertSame([], $result->frontiers);
293 self::assertSame([], $result->exceptionalOutcomes);
294 }
295 public function testScanIndexesConditionalDeclarationsAsCandidateOrigins(): void
296 {
297 $source = '<?php if ($flag) { function conditional(){} } function always(){}';
298 $snapshotId = 'test';
299 $input = new Sources([new Source('fixture.php', $source)]);
300 $profile = new TargetProfile();
301 $parser = new Parser(new ParserConfig($profile->version));
302 $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));
303 $index = new Index($snapshotId, $trees);
304 self::assertSame(['always', 'conditional', 'script:fixture.php'], $index->symbols());
305 }
306 public function testClassDeclarationRetainsInheritanceAndInterfaces(): void
307 {
308 $source = '<?php final class Child extends ParentClass implements Service {use Shared; public function run(){}}';
309 $snapshotId = 'test';
310 $input = new Sources([new Source('fixture.php', $source)]);
311 $profile = new TargetProfile();
312 $parser = new Parser(new ParserConfig($profile->version));
313 $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));
314 $index = new Index($snapshotId, $trees);
315 $class = $index->classes()['child'];
316 self::assertSame('ParentClass', $class->parent);
317 self::assertSame(['Service'], $class->interfaces);
318 self::assertSame(['Shared'], $class->traits);
319 self::assertTrue($class->final);
320 self::assertSame('Child::run', $class->methods['run']);
321 }
322 public function testPropertiesRetainsVisibilityReadonlyAndUnevaluatedDefaults(): void
323 {
324 $source = '<?php class Record {protected static int $count = 1+2; public readonly string $id;}';
325 $snapshotId = 'test';
326 $input = new Sources([new Source('fixture.php', $source)]);
327 $profile = new TargetProfile();
328 $parser = new Parser(new ParserConfig($profile->version));
329 $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));
330 $index = new Index($snapshotId, $trees);
331 $properties = $index->classes()['record']->properties;
332 self::assertSame(Visibility::Protected, $properties['count']->visibility);
333 self::assertTrue($properties['count']->static);
334 self::assertNotNull($properties['count']->default);
335 self::assertTrue($properties['id']->readonly);
336 }
337 public function testConstantsKeepsEnumCaseIdentityAndBackingValues(): void
338 {
339 $source = '<?php enum Status: string {case Ready = "ready";}';
340 $snapshotId = 'test';
341 $input = new Sources([new Source('fixture.php', $source)]);
342 $profile = new TargetProfile();
343 $parser = new Parser(new ParserConfig($profile->version));
344 $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));
345 $index = new Index($snapshotId, $trees);
346 $ready = $index->classes()['status']->constants['Ready'];
347 self::assertSame(LiteralKind::Enum, $ready->kind);
348 self::assertSame('ready', $ready->operands['value']->native());
349 }
350 public function testPromotionsDoesNotUseTheParameterDefaultAsAPropertyDefault(): void
351 {
352 $source = '<?php class Record {public function __construct(public readonly int $id = 3){}}';
353 $snapshotId = 'test';
354 $input = new Sources([new Source('fixture.php', $source)]);
355 $profile = new TargetProfile();
356 $parser = new Parser(new ParserConfig($profile->version));
357 $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));
358 $index = new Index($snapshotId, $trees);
359 $property = $index->classes()['record']->properties['id'];
360 self::assertTrue($property->readonly);
361 self::assertSame('int', $property->type);
362 self::assertNull($property->default);
363 }
364 /**
365 * @param list<Stmt> $nodes File declarations
366 * @param bool $expected Strict scalar coercion mode
367 */
368 #[DataProvider('providerStrictDeclarations')]
369 public function testStrictReadsOnlyTheIntegerStrictTypesDirective(array $nodes, bool $expected): void
370 {
371 $source = '<?php function target(){return 1;}';
372 $snapshotId = 'test';
373 $input = new Sources([new Source('fixture.php', $source)]);
374 $profile = new TargetProfile();
375 $parser = new Parser(new ParserConfig($profile->version));
376 $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));
377 self::assertSame($expected, (new Indexer(new Index($snapshotId, $trees)))->strict($nodes));
378 }
379 /**
380 * @return iterable<string,array{list<Stmt>,bool}>
381 */
382 public static function providerStrictDeclarations(): iterable
383 {
384 yield 'absent' => [[], false];
385 yield 'unrelated statement' => [[new Nop()], false];
386 yield 'enabled' => [[new Declare_([new DeclareItem('strict_types', new Int_(1))])], true];
387 yield 'disabled' => [[new Declare_([new DeclareItem('strict_types', new Int_(0))])], false];
388 yield 'other integer' => [[new Declare_([new DeclareItem('strict_types', new Int_(2))])], false];
389 yield 'string directive' => [[new Declare_([new DeclareItem('strict_types', new String_('1'))])], false];
390 yield 'unrelated directive' => [[new Declare_([new DeclareItem('ticks', new Int_(1))])], false];
391 yield 'multiple declarations' => [[new Declare_([new DeclareItem('ticks', new Int_(1))]), new Declare_([new DeclareItem('strict_types', new Int_(1))])], true];
392 yield 'multiple items' => [[new Declare_([new DeclareItem('ticks', new Int_(1)), new DeclareItem('strict_types', new Int_(1))])], true];
393 }
394 /**
395 * @param string $declaration Declaration syntax
396 * @param array{bool,bool,bool,bool,bool} $flags Final, abstract, interface, readonly, enum
397 * @param string $parent Declared parent
398 * @param list<string> $interfaces Declared interfaces
399 */
400 #[DataProvider('providerClassKinds')]
401 public function testClassDeclarationPreservesEachDeclarationKind(string $declaration, array $flags, string $parent, array $interfaces): void
402 {
403 $source = '<?php namespace App; ' . $declaration;
404 $snapshotId = 'test';
405 $input = new Sources([new Source('fixture.php', $source)]);
406 $profile = new TargetProfile();
407 $parser = new Parser(new ParserConfig($profile->version));
408 $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));
409 $index = new Index($snapshotId, $trees);
410 $class = $index->classIndex['app\subject'];
411 self::assertSame('App\Subject', $class->name);
412 self::assertSame($flags, [$class->final, $class->abstract, $class->interface, $class->readonly, $class->enum]);
413 self::assertSame($parent, $class->parent);
414 self::assertSame($interfaces, $class->interfaces);
415 self::assertSame('App\Subject', $index->classSources['app\subject']->className);
416 self::assertSame('fixture.php', $index->classSources['app\subject']->path);
417 }
418 /**
419 * @return iterable<string,array{string,array{bool,bool,bool,bool,bool},string,list<string>}>
420 */
421 public static function providerClassKinds(): iterable
422 {
423 yield 'ordinary' => ['class Subject{}', [false, false, false, false, false], '', []];
424 yield 'final' => ['final class Subject{}', [true, false, false, false, false], '', []];
425 yield 'abstract' => ['abstract class Subject{}', [false, true, false, false, false], '', []];
426 yield 'readonly' => ['readonly class Subject{}', [false, false, false, true, false], '', []];
427 yield 'final readonly' => ['final readonly class Subject{}', [true, false, false, true, false], '', []];
428 yield 'inheritance' => ['class Subject extends ParentType implements One,Two{}', [false, false, false, false, false], 'App\ParentType', ['App\One', 'App\Two']];
429 yield 'trait' => ['trait Subject{}', [false, true, false, false, false], '', []];
430 yield 'interface' => ['interface Subject extends One,Two{}', [false, false, true, false, false], '', ['App\One', 'App\Two']];
431 yield 'unit enum' => ['enum Subject{case Ready;}', [true, false, false, false, true], '', []];
432 yield 'backed enum' => ['enum Subject:string implements One{case Ready="ready";}', [true, false, false, false, true], '', ['App\One']];
433 }
434 public function testScanRetainsNamespaceCaseAndStrictModeWithoutLoweringBodies(): void
435 {
436 $source = '<?php declare(strict_types=1);namespace App{function MixedCase(){return nested();}const Key=4,Other=5;class Subject{function Run(){return 7;}}}namespace Other{function Call(){return 8;}}';
437 $snapshotId = 'test';
438 $input = new Sources([new Source('fixture.php', $source)]);
439 $profile = new TargetProfile();
440 $parser = new Parser(new ParserConfig($profile->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($input, false));
442 $index = new Index($snapshotId, $trees);
443 self::assertSame('App\MixedCase', $index->declarations['app\mixedcase']->symbol);
444 self::assertTrue($index->declarations['app\mixedcase']->strict);
445 self::assertSame('fixture.php', $index->declarations['app\mixedcase']->path);
446 self::assertSame('App\Subject', $index->declarations['app\subject::run']->className);
447 self::assertTrue($index->declarations['app\subject::run']->strict);
448 self::assertSame(['run' => 'App\Subject::Run'], $index->classIndex['app\subject']->methods);
449 self::assertSame(['App\Key', 'App\Other'], array_keys($index->constantSources));
450 self::assertTrue($index->constantSources['App\Other']->strict);
451 self::assertArrayHasKey('other\call', $index->declarations);
452 self::assertSame([], $index->graphs);
453 }
454 public function testScanUsesUnqualifiedNamesWhenNoResolverAnnotationsExist(): void
455 {
456 $source = '<?php';
457 $snapshotId = 'test';
458 $input = new Sources([new Source('fixture.php', $source)]);
459 $profile = new TargetProfile();
460 $parser = new Parser(new ParserConfig($profile->version));
461 $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));
462 $index = new Index($snapshotId, $trees);
463 $nodes = (new ParserFactory())->createForNewestSupportedVersion()->parse('<?php function Plain(){} const Key=7; class Subject{function Run(){}}');
464 self::assertNotNull($nodes);
465 self::assertInstanceOf(Function_::class, $nodes[0]);
466 self::assertInstanceOf(Const_::class, $nodes[1]);
467 self::assertInstanceOf(Class_::class, $nodes[2]);
468 $nodes[0]->namespacedName = null;
469 $nodes[1]->consts[0]->namespacedName = null;
470 $nodes[2]->namespacedName = null;
471 (new Indexer($index))->scan($nodes, 'fixture.php', true);
472 self::assertSame('Plain', $index->declarations['plain']->symbol);
473 self::assertSame('Key', $index->constantSources['Key']->symbol);
474 self::assertSame('Subject', $index->classSources['subject']->symbol);
475 self::assertSame(['run' => 'Subject::Run'], $index->classIndex['subject']->methods);
476 self::assertTrue($index->classSources['subject']->strict);
477 }
478 public function testScanLeavesNestedAndAnonymousDeclarationsToRuntimeBoundaries(): void
479 {
480 $source = '<?php function Outer(){function Nested(){} class Local{}} if($flag){class Conditional{}} $a=new class{function Run(){}};';
481 $snapshotId = 'test';
482 $input = new Sources([new Source('fixture.php', $source)]);
483 $profile = new TargetProfile();
484 $parser = new Parser(new ParserConfig($profile->version));
485 $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));
486 $index = new Index($snapshotId, $trees);
487 self::assertSame(['Outer', 'script:fixture.php'], $index->symbols());
488 self::assertSame(['conditional'], array_keys($index->classIndex));
489 self::assertSame([], $index->constantSources);
490 }
491 public function testClassDeclarationPreservesTraitUseOrderAndMethodNames(): void
492 {
493 $source = '<?php namespace App;trait One{}trait Two{}trait Three{}class Subject{use One,Two;use Three;function MixedCase(){}function next(){}}';
494 $snapshotId = 'test';
495 $input = new Sources([new Source('fixture.php', $source)]);
496 $profile = new TargetProfile();
497 $parser = new Parser(new ParserConfig($profile->version));
498 $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));
499 $index = new Index($snapshotId, $trees);
500 $class = $index->classIndex['app\subject'];
501 self::assertSame(['App\One', 'App\Two', 'App\Three'], $class->traits);
502 self::assertSame(['mixedcase' => 'App\Subject::MixedCase', 'next' => 'App\Subject::next'], $class->methods);
503 self::assertSame('App\Subject', $index->declarations['app\subject::mixedcase']->className);
504 }
505 /**
506 * @param string $declaration Source property declaration
507 * @param bool $readonlyClass Enclosing class modifier
508 * @param string $visibility Visibility
509 * @param bool $static Whether storage belongs to the class
510 * @param bool $readonly Whether reassignment is forbidden
511 * @param string $type Declared type
512 */
513 #[DataProvider('providerPropertyDeclarations')]
514 public function testPropertiesPreservesTypesStorageAndVisibility(string $declaration, bool $readonlyClass, string $visibility, bool $static, bool $readonly, string $type): void
515 {
516 $source = '<?php ' . ($readonlyClass ? 'readonly ' : '') . 'class Subject{' . $declaration . '}';
517 $snapshotId = 'test';
518 $input = new Sources([new Source('fixture.php', $source)]);
519 $profile = new TargetProfile();
520 $parser = new Parser(new ParserConfig($profile->version));
521 $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));
522 $index = new Index($snapshotId, $trees);
523 $property = $index->classIndex['subject']->properties['value'];
524 self::assertSame('value', $property->name);
525 self::assertSame('Subject', $property->className);
526 self::assertSame(Visibility::from($visibility), $property->visibility);
527 self::assertSame($static, $property->static);
528 self::assertSame($readonly, $property->readonly);
529 self::assertSame($type, $property->type);
530 self::assertNull($property->default);
531 }
532 /**
533 * @return iterable<string,array{string,bool,string,bool,bool,string}>
534 */
535 public static function providerPropertyDeclarations(): iterable
536 {
537 yield 'untyped' => ['public $value;', false, 'public', false, false, 'mixed'];
538 yield 'private typed' => ['private int $value;', false, 'private', false, false, 'int'];
539 yield 'protected nullable' => ['protected ?string $value;', false, 'protected', false, false, 'string|null'];
540 yield 'public union' => ['public int|string $value;', false, 'public', false, false, 'int|string'];
541 yield 'private static' => ['private static int $value;', false, 'private', true, false, 'int'];
542 yield 'protected static' => ['protected static array $value;', false, 'protected', true, false, 'array'];
543 yield 'explicit readonly' => ['public readonly int $value;', false, 'public', false, true, 'int'];
544 yield 'class readonly' => ['protected int $value;', true, 'protected', false, true, 'int'];
545 yield 'both readonly' => ['public readonly int $value;', true, 'public', false, true, 'int'];
546 }
547 public function testPropertiesKeepsEachDefaultInItsOwnLexicalGraph(): void
548 {
549 $source = '<?php namespace App;class Subject{public int $first=3,$second=4;public $absent;}';
550 $snapshotId = 'test';
551 $input = new Sources([new Source('fixture.php', $source)]);
552 $profile = new TargetProfile();
553 $parser = new Parser(new ParserConfig($profile->version));
554 $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));
555 $index = new Index($snapshotId, $trees);
556 $properties = $index->classIndex['app\subject']->properties;
557 self::assertSame(['first', 'second', 'absent'], array_keys($properties));
558 self::assertNotNull($properties['first']->default);
559 self::assertNotNull($properties['second']->default);
560 self::assertSame('App\Subject::$first', $properties['first']->default->symbol);
561 self::assertSame('App\Subject::$second', $properties['second']->default->symbol);
562 self::assertSame('App\Subject', $properties['first']->default->className);
563 self::assertSame(3, $properties['first']->default->blocks[0]->instructions[0]->constant?->native());
564 self::assertSame(4, $properties['second']->default->blocks[0]->instructions[0]->constant?->native());
565 self::assertNull($properties['absent']->default);
566 }
567 public function testConstantsDistinguishesScalarLiteralsFromUnevaluatedInitializers(): void
568 {
569 $source = '<?php class Subject{const TEXT="bytes",NUMBER=3,FRACTION=1.5,EXPR=1+2,FLAG=true;}';
570 $snapshotId = 'test';
571 $input = new Sources([new Source('fixture.php', $source)]);
572 $profile = new TargetProfile();
573 $parser = new Parser(new ParserConfig($profile->version));
574 $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));
575 $index = new Index($snapshotId, $trees);
576 $constants = $index->classIndex['subject']->constants;
577 self::assertSame(['TEXT', 'NUMBER', 'FRACTION', 'EXPR', 'FLAG'], array_keys($constants));
578 self::assertSame('bytes', $constants['TEXT']->native());
579 self::assertSame(3, $constants['NUMBER']->native());
580 self::assertSame(1.5, $constants['FRACTION']->native());
581 self::assertSame(LiteralKind::Unresolved, $constants['EXPR']->kind);
582 self::assertSame('UNSUPPORTED_LANGUAGE_FEATURE', $constants['EXPR']->literal);
583 self::assertSame(LiteralKind::Unresolved, $constants['FLAG']->kind);
584 self::assertSame([], $constants['TEXT']->operands);
585 }
586 /**
587 * @param string $source Enum declaration
588 * @param array<string,int|string> $fields Expected enum fields
589 */
590 #[DataProvider('providerEnumCases')]
591 public function testConstantsKeepsNamespacedEnumIdentityAndOptionalBackingValues(string $source, array $fields): void
592 {
593 $source1 = '<?php namespace App;' . $source;
594 $snapshotId = 'test';
595 $input = new Sources([new Source('fixture.php', $source1)]);
596 $profile = new TargetProfile();
597 $parser = new Parser(new ParserConfig($profile->version));
598 $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input, false));
599 $index = new Index($snapshotId, $trees);
600 $case = $index->classIndex['app\subject']->constants['Ready'];
601 self::assertSame(LiteralKind::Enum, $case->kind);
602 self::assertSame('App\Subject::Ready', $case->literal);
603 self::assertSame('App\Subject', $case->attributes['class']);
604 self::assertSame($fields, array_map(static fn (Term $value): int|float|string|bool|null => $value->literal, $case->operands));
605 }
606 /**
607 * @return iterable<string,array{string,array<string,int|string>}>
608 */
609 public static function providerEnumCases(): iterable
610 {
611 yield 'unit' => ['enum Subject{case Ready;}', ['name' => 'Ready']];
612 yield 'integer backed' => ['enum Subject:int{case Ready=4;}', ['name' => 'Ready', 'value' => 4]];
613 yield 'string backed' => ['enum Subject:string{case Ready="ready";}', ['name' => 'Ready', 'value' => 'ready']];
614 }
615 /**
616 * @param string $parameter Promoted parameter declaration
617 * @param bool $readonlyClass Class modifier
618 * @param string $visibility Expected visibility
619 * @param bool $readonly Expected readonly contract
620 */
621 #[DataProvider('providerPromotedProperties')]
622 public function testPromotionsCapturesPropertyModifiersWithoutCopyingParameterDefaults(string $parameter, bool $readonlyClass, string $visibility, bool $readonly): void
623 {
624 $source = '<?php ' . ($readonlyClass ? 'readonly ' : '') . 'class Subject{function __construct(' . $parameter . ',int $ordinary=9){}}';
625 $snapshotId = 'test';
626 $input = new Sources([new Source('fixture.php', $source)]);
627 $profile = new TargetProfile();
628 $parser = new Parser(new ParserConfig($profile->version));
629 $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));
630 $index = new Index($snapshotId, $trees);
631 $properties = $index->classIndex['subject']->properties;
632 self::assertSame(['value'], array_keys($properties));
633 self::assertSame('value', $properties['value']->name);
634 self::assertSame('Subject', $properties['value']->className);
635 self::assertSame('int', $properties['value']->type);
636 self::assertSame(Visibility::from($visibility), $properties['value']->visibility);
637 self::assertSame($readonly, $properties['value']->readonly);
638 self::assertFalse($properties['value']->static);
639 self::assertNull($properties['value']->default);
640 }
641 /**
642 * @return iterable<string,array{string,bool,string,bool}>
643 */
644 public static function providerPromotedProperties(): iterable
645 {
646 yield 'public' => ['public int $value=3', false, 'public', false];
647 yield 'protected' => ['protected int $value=3', false, 'protected', false];
648 yield 'private' => ['private int $value=3', false, 'private', false];
649 yield 'readonly' => ['public readonly int $value=3', false, 'public', true];
650 yield 'readonly class' => ['private int $value=3', true, 'private', true];
651 yield 'both readonly' => ['protected readonly int $value=3', true, 'protected', true];
652 }
653 public function testExistingClassRejectsDuplicateSourceDeclarations(): void
654 {
655 $source = '<?php class C{}';
656 $snapshotId = 'test';
657 $input = new Sources([new Source('fixture.php', $source)]);
658 $profile = new TargetProfile();
659 $parser = new Parser(new ParserConfig($profile->version));
660 $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));
661 $index = new Index($snapshotId, $trees);
662 $node = new Class_('C');
663 self::assertTrue((new Indexer($index))->existingClass($node, 'fixture.php', 'C'));
664 self::assertSame('INVALID_PROGRAM', $index->diagnostics()[0]->code);
665 }
666 public function testConditionalMethodsRetainsBothImplementations(): void
667 {
668 $source = '<?php if($x){class A{function f(){return 1;}}}else{class A{function f(){return 2;}}}';
669 $snapshotId = 'test';
670 $input = new Sources([new Source('fixture.php', $source)]);
671 $profile = new TargetProfile();
672 $parser = new Parser(new ParserConfig($profile->version));
673 $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));
674 $index = new Index($snapshotId, $trees);
675 self::assertCount(1, $index->variants('A::f'));
676 self::assertSame([], $index->diagnostics());
677 }
678}
679