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