diff --git a/src/Infer/Services/KeyedArrayUnpackingTypeVisitor.php b/src/Infer/Services/KeyedArrayUnpackingTypeVisitor.php new file mode 100644 index 000000000..a3eb2d61b --- /dev/null +++ b/src/Infer/Services/KeyedArrayUnpackingTypeVisitor.php @@ -0,0 +1,20 @@ +replace( - $resolvedType, - fn (Type $t) => $t instanceof Union ? TypeHelper::mergeTypes(...$t->types) : null, - ); - - return $this->resolveLateTypes($finalizedResolvedType->setOriginal($originalType), $originalType)->widen(); + return $this->finalizeType($resolvedType->setOriginal($originalType), $originalType)->widen(); } private function doResolve(Type $t, Type $type, Scope $scope): Type @@ -121,11 +114,13 @@ private function resolveLateTypeEarly(LateResolvingType $type): Type return $type->resolve(); } - private function resolveLateTypes(Type $type, Type $originalType): Type + private function finalizeType(Type $type, Type $originalType): Type { $attributes = $type->attributes(); $traverser = new TypeTraverser([ + new UnionNormalizingTypeVisitor, + new KeyedArrayUnpackingTypeVisitor, new LateTypeResolvingTypeVisitor, ]); diff --git a/src/Infer/Services/UnionNormalizingTypeVisitor.php b/src/Infer/Services/UnionNormalizingTypeVisitor.php new file mode 100644 index 000000000..d07e0a0c2 --- /dev/null +++ b/src/Infer/Services/UnionNormalizingTypeVisitor.php @@ -0,0 +1,20 @@ +types); + } +} diff --git a/src/Support/Type/TypeTraverser.php b/src/Support/Type/TypeTraverser.php index dfe04008d..5b45d7bbf 100644 --- a/src/Support/Type/TypeTraverser.php +++ b/src/Support/Type/TypeTraverser.php @@ -49,20 +49,38 @@ public function traverse(Type $type): Type private function enterType(Type $type): Type|int|null { $result = null; + $resultType = $type; foreach ($this->visitors as $visitor) { - $result = $visitor->enter($result instanceof Type ? $result : $type); + $enterResult = $visitor->enter($resultType); + + if ($enterResult === TypeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { + return $enterResult; + } + + if ($enterResult instanceof Type) { + $resultType = $enterResult; + } + + $result = $enterResult; } - return $result; + return $resultType === $type ? $result : $resultType; } private function leaveType(Type $type): Type|int|null { $result = null; + $resultType = $type; foreach ($this->visitors as $visitor) { - $result = $visitor->leave($result instanceof Type ? $result : $type); + $leaveResult = $visitor->leave($resultType); + + if ($leaveResult instanceof Type) { + $resultType = $leaveResult; + } + + $result = $leaveResult; } - return $result; + return $resultType === $type ? $result : $resultType; } } diff --git a/tests/Infer/Services/ReferenceTypeResolverTest.php b/tests/Infer/Services/ReferenceTypeResolverTest.php index f50afc7d0..4f5d73b7e 100644 --- a/tests/Infer/Services/ReferenceTypeResolverTest.php +++ b/tests/Infer/Services/ReferenceTypeResolverTest.php @@ -231,3 +231,38 @@ public function toString(): string ]), )->toString())->toBe('string(wow)'); }); + +it('resolves spread in union type', function () { + $def = analyzeFile(<<<'EOD' + [ + ...$this->typeA(), + 'type' => 'a', + ], + 1 => [ + ...$this->typeB(), + 'type' => 'b', + ], + }; + } + + private function typeA(): array + { + return ['id' => 1, 'name' => 'Type A']; + } + + private function typeB(): array + { + return ['id' => 2, 'name' => 'Type B']; + } +} +EOD)->getClassDefinition('JsonResourceExtensionTest_SpreadInMatch'); + + expect($def->getMethod('toArray')->getReturnType()->toString()) + ->toBe('array{id: int(1), name: string(Type A), type: string(a)}|array{id: int(2), name: string(Type B), type: string(b)}'); +});