Skip to content

Commit 0c14d1d

Browse files
Fixed keyed array types being properly unpacked when are parts of union (#1094)
* fixed unpacked types being not handled in unions * Fix styling --------- Co-authored-by: romalytvynenko <romalytvynenko@users.noreply.github.com>
1 parent 9c4a758 commit 0c14d1d

5 files changed

Lines changed: 101 additions & 13 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Dedoc\Scramble\Infer\Services;
4+
5+
use Dedoc\Scramble\Support\Type\AbstractTypeVisitor;
6+
use Dedoc\Scramble\Support\Type\KeyedArrayType;
7+
use Dedoc\Scramble\Support\Type\Type;
8+
use Dedoc\Scramble\Support\Type\TypeHelper;
9+
10+
class KeyedArrayUnpackingTypeVisitor extends AbstractTypeVisitor
11+
{
12+
public function leave(Type $type): ?Type
13+
{
14+
if (! $type instanceof KeyedArrayType) {
15+
return null;
16+
}
17+
18+
return TypeHelper::unpackIfArray($type);
19+
}
20+
}

src/Infer/Services/ReferenceTypeResolver.php

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,8 @@
3232
use Dedoc\Scramble\Support\Type\TemplatePlaceholderType;
3333
use Dedoc\Scramble\Support\Type\TemplateType;
3434
use Dedoc\Scramble\Support\Type\Type;
35-
use Dedoc\Scramble\Support\Type\TypeHelper;
3635
use Dedoc\Scramble\Support\Type\TypeTraverser;
3736
use Dedoc\Scramble\Support\Type\TypeWalker;
38-
use Dedoc\Scramble\Support\Type\Union;
3937
use Dedoc\Scramble\Support\Type\UnknownType;
4038
use Dedoc\Scramble\Support\Type\VoidType;
4139

@@ -61,12 +59,7 @@ public function resolve(Scope $scope, Type $type): Type
6159
);
6260

6361
// Type finalization: removing duplicates from union, unpacking array items (inside `replace`), calling resolving extensions.
64-
$finalizedResolvedType = (new TypeWalker)->replace(
65-
$resolvedType,
66-
fn (Type $t) => $t instanceof Union ? TypeHelper::mergeTypes(...$t->types) : null,
67-
);
68-
69-
return $this->resolveLateTypes($finalizedResolvedType->setOriginal($originalType), $originalType)->widen();
62+
return $this->finalizeType($resolvedType->setOriginal($originalType), $originalType)->widen();
7063
}
7164

7265
private function doResolve(Type $t, Type $type, Scope $scope): Type
@@ -121,11 +114,13 @@ private function resolveLateTypeEarly(LateResolvingType $type): Type
121114
return $type->resolve();
122115
}
123116

124-
private function resolveLateTypes(Type $type, Type $originalType): Type
117+
private function finalizeType(Type $type, Type $originalType): Type
125118
{
126119
$attributes = $type->attributes();
127120

128121
$traverser = new TypeTraverser([
122+
new UnionNormalizingTypeVisitor,
123+
new KeyedArrayUnpackingTypeVisitor,
129124
new LateTypeResolvingTypeVisitor,
130125
]);
131126

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Dedoc\Scramble\Infer\Services;
4+
5+
use Dedoc\Scramble\Support\Type\AbstractTypeVisitor;
6+
use Dedoc\Scramble\Support\Type\Type;
7+
use Dedoc\Scramble\Support\Type\TypeHelper;
8+
use Dedoc\Scramble\Support\Type\Union;
9+
10+
class UnionNormalizingTypeVisitor extends AbstractTypeVisitor
11+
{
12+
public function leave(Type $type): ?Type
13+
{
14+
if (! $type instanceof Union) {
15+
return null;
16+
}
17+
18+
return TypeHelper::mergeTypes(...$type->types);
19+
}
20+
}

src/Support/Type/TypeTraverser.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,38 @@ public function traverse(Type $type): Type
4949
private function enterType(Type $type): Type|int|null
5050
{
5151
$result = null;
52+
$resultType = $type;
5253
foreach ($this->visitors as $visitor) {
53-
$result = $visitor->enter($result instanceof Type ? $result : $type);
54+
$enterResult = $visitor->enter($resultType);
55+
56+
if ($enterResult === TypeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN) {
57+
return $enterResult;
58+
}
59+
60+
if ($enterResult instanceof Type) {
61+
$resultType = $enterResult;
62+
}
63+
64+
$result = $enterResult;
5465
}
5566

56-
return $result;
67+
return $resultType === $type ? $result : $resultType;
5768
}
5869

5970
private function leaveType(Type $type): Type|int|null
6071
{
6172
$result = null;
73+
$resultType = $type;
6274
foreach ($this->visitors as $visitor) {
63-
$result = $visitor->leave($result instanceof Type ? $result : $type);
75+
$leaveResult = $visitor->leave($resultType);
76+
77+
if ($leaveResult instanceof Type) {
78+
$resultType = $leaveResult;
79+
}
80+
81+
$result = $leaveResult;
6482
}
6583

66-
return $result;
84+
return $resultType === $type ? $result : $resultType;
6785
}
6886
}

tests/Infer/Services/ReferenceTypeResolverTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,38 @@ public function toString(): string
231231
]),
232232
)->toString())->toBe('string(wow)');
233233
});
234+
235+
it('resolves spread in union type', function () {
236+
$def = analyzeFile(<<<'EOD'
237+
<?php
238+
class JsonResourceExtensionTest_SpreadInMatch
239+
{
240+
public function toArray(Request $request)
241+
{
242+
return match (mt_rand(0, 1)) {
243+
0 => [
244+
...$this->typeA(),
245+
'type' => 'a',
246+
],
247+
1 => [
248+
...$this->typeB(),
249+
'type' => 'b',
250+
],
251+
};
252+
}
253+
254+
private function typeA(): array
255+
{
256+
return ['id' => 1, 'name' => 'Type A'];
257+
}
258+
259+
private function typeB(): array
260+
{
261+
return ['id' => 2, 'name' => 'Type B'];
262+
}
263+
}
264+
EOD)->getClassDefinition('JsonResourceExtensionTest_SpreadInMatch');
265+
266+
expect($def->getMethod('toArray')->getReturnType()->toString())
267+
->toBe('array{id: int(1), name: string(Type A), type: string(a)}|array{id: int(2), name: string(Type B), type: string(b)}');
268+
});

0 commit comments

Comments
 (0)