packages/deriver/tests/Unit/Deriver/Resolver/AssignmentPatternsTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Resolver;
6
7use Deriver\Deriver\Parser\Node\File;
8use Deriver\Deriver\Parser\Parser;
9use Deriver\Deriver\Parser\ParserConfig;
10use Deriver\Deriver\Parser\TargetSyntax;
11use Deriver\Deriver\Resolver\AssignmentPatterns;
12use Deriver\Deriver\Resolver\ClassScope;
13use Deriver\Deriver\Resolver\ExpressionValidation;
14use Deriver\Deriver\Resolver\MagicContext;
15use Deriver\Deriver\Resolver\Resolver;
16use Deriver\Deriver\Sources\Source;
17use Deriver\Exception\ResolutionException;
18use PhpParser\ErrorHandler\Collecting;
19use PhpParser\Node\ArrayItem;
20use PhpParser\Node\Expr;
21use PhpParser\Node\Expr\Array_;
22use PhpParser\Node\Expr\ArrayDimFetch;
23use PhpParser\Node\Expr\FuncCall;
24use PhpParser\Node\Expr\List_;
25use PhpParser\Node\Expr\MethodCall;
26use PhpParser\Node\Expr\New_;
27use PhpParser\Node\Expr\NullsafeMethodCall;
28use PhpParser\Node\Expr\NullsafePropertyFetch;
29use PhpParser\Node\Expr\PropertyFetch;
30use PhpParser\Node\Expr\StaticCall;
31use PhpParser\Node\Expr\StaticPropertyFetch;
32use PhpParser\Node\Expr\Variable;
33use PhpParser\Node\Name;
34use PhpParser\Node\Scalar\Int_;
35use PHPUnit\Framework\Attributes\CoversClass;
36use PHPUnit\Framework\Attributes\DataProvider;
37use PHPUnit\Framework\Attributes\Small;
38use PHPUnit\Framework\Attributes\UsesClass;
39use PHPUnit\Framework\TestCase;
40
41#[UsesClass(Resolver::class)]
42#[UsesClass(ExpressionValidation::class)]
43#[UsesClass(ResolutionException::class)]
44#[UsesClass(ParserConfig::class)]
45#[CoversClass(AssignmentPatterns::class)]
46#[UsesClass(Parser::class)]
47#[UsesClass(File::class)]
48#[UsesClass(MagicContext::class)]
49#[UsesClass(ClassScope::class)]
50#[UsesClass(TargetSyntax::class)]
51#[UsesClass(Source::class)]
52#[Small]
53final class AssignmentPatternsTest extends TestCase
54{
55    #[DataProvider('providerProgramsDestructuringProgramsinvalid')]
56    public function testEnterNodeRejectsPatternsBeforeExecutingAnyCallable(string $source, string $message): void
57    {
58        $this->expectException(ResolutionException::class);
59        $this->expectExceptionMessage($message);
60        (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('fixture.php', $source)));
61    }
62    #[DataProvider('providerValidPatterns')]
63    public function testPatternAcceptsHomogeneousNestedAndKeyedPatterns(List_ $pattern): void
64    {
65        self::assertSame([], (new AssignmentPatterns(new Collecting()))->pattern($pattern));
66    }
67    /**
68     * @return iterable<string,array{List_}>
69     */
70    public static function providerValidPatterns(): iterable
71    {
72        yield 'unkeyed holes' => [new List_([null, new ArrayItem(new Variable('x'))])];
73        yield 'explicit keys' => [new List_([new ArrayItem(new Variable('x'), new Int_(2)), new ArrayItem(new Variable('y'), new Int_(4))])];
74        yield 'nested lists' => [new List_([new ArrayItem(new List_([new ArrayItem(new Variable('x'))]))])];
75    }
76    #[DataProvider('providerSourceSyntax')]
77    public function testSourceErrorRejectsInvalidRootsAndNullsafeChains(Expr $source, string $expected): void
78    {
79        self::assertSame($expected, (new AssignmentPatterns(new Collecting()))->sourceError($source));
80    }
81    /**
82     * @return iterable<string,array{Expr,string}>
83     */
84    public static function providerSourceSyntax(): iterable
85    {
86        yield 'variable' => [new Variable('source'), ''];
87        yield 'array element' => [new ArrayDimFetch(new Variable('source'), new Int_(0)), ''];
88        yield 'property' => [new PropertyFetch(new Variable('source'), 'value'), ''];
89        yield 'static property' => [new StaticPropertyFetch(new Name('Box'), 'value'), ''];
90        yield 'function' => [new FuncCall(new Name('source')), ''];
91        yield 'method' => [new MethodCall(new Variable('source'), 'value'), ''];
92        yield 'static method' => [new StaticCall(new Name('Box'), 'value'), ''];
93        yield 'literal' => [new Int_(1), 'Cannot assign reference to non referenceable value.'];
94        yield 'array literal' => [new Array_([]), 'Cannot assign reference to non referenceable value.'];
95        yield 'temporary property' => [new PropertyFetch(new New_(new Name('Box')), 'value'), 'Cannot use temporary expression in write context.'];
96        yield 'temporary array element' => [new ArrayDimFetch(new Array_([]), new Int_(0)), 'Cannot use temporary expression in write context.'];
97        yield 'nullsafe property' => [new NullsafePropertyFetch(new Variable('source'), 'value'), 'Cannot take reference of a nullsafe chain.'];
98        yield 'nullsafe method' => [new NullsafeMethodCall(new Variable('source'), 'value'), 'Cannot take reference of a nullsafe chain.'];
99        yield 'nested nullsafe chain' => [new ArrayDimFetch(new NullsafePropertyFetch(new Variable('source'), 'value'), new Int_(0)), 'Cannot take reference of a nullsafe chain.'];
100    }
101    #[DataProvider('providerReferencePatterns')]
102    public function testReferencesFindsNestedReferenceLeavesWithoutTreatingEveryPatternAsShared(Expr $pattern, bool $expected): void
103    {
104        self::assertSame($expected, AssignmentPatterns::references($pattern));
105    }
106    /**
107     * @return iterable<string,array{Expr,bool}>
108     */
109    public static function providerReferencePatterns(): iterable
110    {
111        yield 'ordinary variable' => [new Variable('x'), false];
112        yield 'ordinary list' => [new List_([new ArrayItem(new Variable('x'))]), false];
113        yield 'list holes' => [new List_([null, new ArrayItem(new Variable('x'))]), false];
114        yield 'reference list' => [new List_([new ArrayItem(new Variable('x'), byRef: true)]), true];
115        yield 'nested reference' => [new List_([null, new ArrayItem(new Array_([new ArrayItem(new Variable('x'), byRef: true)]))]), true];
116        yield 'nested value' => [new List_([new ArrayItem(new List_([new ArrayItem(new Variable('x'))]))]), false];
117    }
118    /**
119     * @return array<string,array{string,string}> Invalid source and target diagnostic fragment
120     */
121    public static function providerProgramsDestructuringProgramsinvalid(): array
122    {
123        return ['temporary object property source' => ['<?php class Box{public array $items=[1];}function target(){[&$x]=(new Box)->items;}', 'Cannot use temporary expression in write context'], 'literal reference source' => ['<?php function target(){[&$x]=[1];}', 'Cannot assign reference to non referenceable value'], 'scalar reference source' => ['<?php function target(){[&$x]=1;}', 'Cannot assign reference to non referenceable value'], 'nested literal reference source' => ['<?php function target(){[[&$x]]=[[1]];}', 'Cannot assign reference to non referenceable value'], 'empty pattern' => ['<?php function target(){[]=[1];}', 'Cannot use empty list'], 'all holes' => ['<?php function target(){list(,)=[1];}', 'Cannot use empty list'], 'mixed keyed pattern' => ['<?php function target(){[0=>$x,$y]=[1,2];}', 'Cannot mix keyed and unkeyed array entries'], 'keyed hole' => ['<?php function target(){[0=>$x, ,2=>$y]=[1,2,3];}', 'Cannot use empty array entries in keyed array assignment'], 'mixed nested style' => ['<?php function target(){list([$x])=[[1]];}', 'Cannot mix [] and list()'], 'spread pattern' => ['<?php function target(){[$x,...$y]=[1,2];}', 'Spread operator is not supported in assignments'], 'nullsafe reference source' => ['<?php function target($b){[&$x]=$b?->items;}', 'Cannot take reference of a nullsafe chain']];
124    }
125}
126