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