From 380d91de2cad6c35dbd6c59668a635e608e10e82 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:04:10 +0000 Subject: [PATCH 1/5] Fix iterable not accepted as array|Traversable - Fixed IterableType::isSubTypeOf() to use GenericObjectType for the Traversable part of the decomposition instead of IntersectionType with plain ObjectType - The plain ObjectType(Traversable) lost generic type parameters, causing subtype checks against GenericObjectType(Traversable, [K, V]) to return maybe instead of yes - Consistent with how IterableType::tryRemove() already decomposes iterable - New regression test in tests/PHPStan/Rules/Functions/data/bug-13247.php --- src/Type/IterableType.php | 6 +-- .../CallToFunctionParametersRuleTest.php | 5 +++ .../Rules/Functions/data/bug-13247.php | 43 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-13247.php diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 46e004ae235..dd1162cdbbd 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -138,9 +138,9 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) { return $otherType->isSuperTypeOf(new UnionType([ new ArrayType($this->keyType, $this->itemType), - new IntersectionType([ - new ObjectType(Traversable::class), - $this, + new GenericObjectType(Traversable::class, [ + $this->keyType, + $this->itemType, ]), ])); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index d06950ad68f..6bb50169fe6 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2741,4 +2741,9 @@ public function testBug12363(): void $this->analyse([__DIR__ . '/data/bug-12363.php'], []); } + public function testBug13247(): void + { + $this->analyse([__DIR__ . '/data/bug-13247.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-13247.php b/tests/PHPStan/Rules/Functions/data/bug-13247.php new file mode 100644 index 00000000000..911956a5128 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-13247.php @@ -0,0 +1,43 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug13247; + +use Traversable; + +/** + * @param array|Traversable $input + * + * @return array + */ +function as_array(array|Traversable $input): array { + return iter_as_array($input); +} + +/** + * @param iterable $input + * + * @return array + */ +function iter_as_array(iterable $input): array { + return as_array($input); +} + +/** + * @param array|Traversable $input + * + * @return array + */ +function as_array2(array|Traversable $input): array { + return iter_as_array2($input); +} + +/** + * @param iterable $input + * + * @return array + */ +function iter_as_array2(iterable $input): array { + return as_array2($input); +} From eb320ed350048433f6b19ea27cc84d0b3e9a8e70 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 7 Mar 2026 23:00:45 +0100 Subject: [PATCH 2/5] Improve --- src/Type/UnionType.php | 22 +++++++ .../Rules/Functions/data/bug-13247.php | 60 +++++++++++++++++-- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 208e5e3b43a..8d0ba10f9b9 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -25,6 +25,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateIterableType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateType; @@ -33,6 +34,7 @@ use PHPStan\Type\Generic\TemplateUnionType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use Throwable; +use Traversable; use function array_diff_assoc; use function array_fill_keys; use function array_map; @@ -194,6 +196,16 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { + if ($type instanceof IterableType) { + return $this->accepts(new UnionType([ + new ArrayType($type->getIterableKeyType(), $type->getIterableValueType()), + new GenericObjectType(Traversable::class, [ + $type->getIterableKeyType(), + $type->getIterableValueType(), + ]), + ]), $strictTypes); + } + foreach (self::EQUAL_UNION_CLASSES as $baseClass => $classes) { if (!$type->equals(new ObjectType($baseClass))) { continue; @@ -1073,6 +1085,16 @@ public function toCoercedArgumentType(bool $strictTypes): Type public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { + if ($receivedType instanceof IterableType) { + $receivedType = new UnionType([ + new ArrayType($receivedType->getIterableKeyType(), $receivedType->getIterableValueType()), + new GenericObjectType(Traversable::class, [ + $receivedType->getIterableKeyType(), + $receivedType->getIterableValueType(), + ]), + ]); + } + $types = TemplateTypeMap::createEmpty(); if ($receivedType instanceof UnionType) { $myTypes = []; diff --git a/tests/PHPStan/Rules/Functions/data/bug-13247.php b/tests/PHPStan/Rules/Functions/data/bug-13247.php index 911956a5128..c872f2e1be4 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-13247.php +++ b/tests/PHPStan/Rules/Functions/data/bug-13247.php @@ -7,18 +7,24 @@ use Traversable; /** - * @param array|Traversable $input + * @template K as array-key + * @template V * - * @return array + * @param array|Traversable $input + * + * @return array */ function as_array(array|Traversable $input): array { return iter_as_array($input); } /** - * @param iterable $input + * @template K as array-key + * @template V * - * @return array + * @param iterable $input + * + * @return array */ function iter_as_array(iterable $input): array { return as_array($input); @@ -41,3 +47,49 @@ function as_array2(array|Traversable $input): array { function iter_as_array2(iterable $input): array { return as_array2($input); } + +/** + * @param array|Traversable $input + * + * @return array + */ +function as_array3(array|Traversable $input): array { + return iter_as_array3($input); +} + +/** + * @param iterable $input + * + * @return array + */ +function iter_as_array3(iterable $input): array { + return as_array3($input); +} + +/** + * @phpstan-template T of iterable + * + * @param T $input + * + * @return mixed + */ +function test1(iterable $input) { + test2($input); + iter_as_array($input); + + return as_array($input); +} + +/** + * @phpstan-template U of Traversable|array + * + * @param U $input + * + * @return mixed + */ +function test2($input) { + test1($input); + iter_as_array($input); + + return as_array($input); +} From 45dad196b221256c2c8b7cc657bd210cecd3a392 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 7 Mar 2026 23:21:08 +0100 Subject: [PATCH 3/5] Fix --- phpstan-baseline.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3b050733d9f..d6ddf9974a5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1788,7 +1788,7 @@ parameters: - rawMessage: 'Doing instanceof PHPStan\Type\IterableType is error-prone and deprecated. Use Type::isIterable() instead.' identifier: phpstanApi.instanceofType - count: 1 + count: 3 path: src/Type/UnionType.php - From 2dd191394eeb0f6fbd88bce893fb6b96c2e8e5a8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 8 Mar 2026 11:03:37 +0100 Subject: [PATCH 4/5] Introduce method --- src/Type/IterableType.php | 27 +++++++++++++-------------- src/Type/UnionType.php | 16 ++++------------ 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index dd1162cdbbd..d1c1a73bb01 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -137,11 +137,8 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) { return $otherType->isSuperTypeOf(new UnionType([ - new ArrayType($this->keyType, $this->itemType), - new GenericObjectType(Traversable::class, [ - $this->keyType, - $this->itemType, - ]), + $this->toArray(), + $this->toTraversable(), ])); } @@ -250,10 +247,7 @@ public function toCoercedArgumentType(bool $strictTypes): Type ), $this->itemType, ), - new GenericObjectType(Traversable::class, [ - $this->keyType, - $this->itemType, - ]), + $this->toTraversable(), ); } @@ -481,15 +475,12 @@ public function tryRemove(Type $typeToRemove): ?Type { $arrayType = new ArrayType(new MixedType(), new MixedType()); if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) { - return new GenericObjectType(Traversable::class, [ - $this->getIterableKeyType(), - $this->getIterableValueType(), - ]); + return $this->toTraversable(); } $traversableType = new ObjectType(Traversable::class); if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) { - return new ArrayType($this->getIterableKeyType(), $this->getIterableValueType()); + return $this->toArray(); } return null; @@ -537,4 +528,12 @@ public function hasTemplateOrLateResolvableType(): bool return $this->keyType->hasTemplateOrLateResolvableType() || $this->itemType->hasTemplateOrLateResolvableType(); } + public function toTraversable(): Type + { + return new GenericObjectType(Traversable::class, [ + $this->keyType, + $this->itemType, + ]); + } + } diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 8d0ba10f9b9..a5c903db730 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -25,7 +25,6 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateIterableType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateType; @@ -34,7 +33,6 @@ use PHPStan\Type\Generic\TemplateUnionType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use Throwable; -use Traversable; use function array_diff_assoc; use function array_fill_keys; use function array_map; @@ -198,11 +196,8 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof IterableType) { return $this->accepts(new UnionType([ - new ArrayType($type->getIterableKeyType(), $type->getIterableValueType()), - new GenericObjectType(Traversable::class, [ - $type->getIterableKeyType(), - $type->getIterableValueType(), - ]), + $type->toArray(), + $type->toTraversable(), ]), $strictTypes); } @@ -1087,11 +1082,8 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ($receivedType instanceof IterableType) { $receivedType = new UnionType([ - new ArrayType($receivedType->getIterableKeyType(), $receivedType->getIterableValueType()), - new GenericObjectType(Traversable::class, [ - $receivedType->getIterableKeyType(), - $receivedType->getIterableValueType(), - ]), + $receivedType->toArray(), + $receivedType->toTraversable(), ]); } From 0201f7a45d846128ee414f24ef94dd4e67957105 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 8 Mar 2026 11:34:02 +0100 Subject: [PATCH 5/5] Rework --- src/Type/IterableType.php | 36 +++++++++++++++++++++--------------- src/Type/UnionType.php | 10 ++-------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index d1c1a73bb01..2cf46b754e9 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -136,10 +136,7 @@ private function isNestedTypeSuperTypeOf(Type $a, Type $b): IsSuperTypeOfResult public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) { - return $otherType->isSuperTypeOf(new UnionType([ - $this->toArray(), - $this->toTraversable(), - ])); + return $otherType->isSuperTypeOf($this->toArrayOrTraversable()); } if ($otherType instanceof self) { @@ -228,6 +225,17 @@ public function toArray(): Type return new ArrayType($this->keyType, $this->getItemType()); } + public function toArrayOrTraversable(): Type + { + return new UnionType([ + new ArrayType($this->keyType, $this->itemType), + new GenericObjectType(Traversable::class, [ + $this->keyType, + $this->itemType, + ]), + ]); + } + public function toArrayKey(): Type { return new ErrorType(); @@ -247,7 +255,10 @@ public function toCoercedArgumentType(bool $strictTypes): Type ), $this->itemType, ), - $this->toTraversable(), + new GenericObjectType(Traversable::class, [ + $this->keyType, + $this->itemType, + ]), ); } @@ -475,12 +486,15 @@ public function tryRemove(Type $typeToRemove): ?Type { $arrayType = new ArrayType(new MixedType(), new MixedType()); if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) { - return $this->toTraversable(); + return new GenericObjectType(Traversable::class, [ + $this->getIterableKeyType(), + $this->getIterableValueType(), + ]); } $traversableType = new ObjectType(Traversable::class); if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) { - return $this->toArray(); + return new ArrayType($this->getIterableKeyType(), $this->getIterableValueType()); } return null; @@ -528,12 +542,4 @@ public function hasTemplateOrLateResolvableType(): bool return $this->keyType->hasTemplateOrLateResolvableType() || $this->itemType->hasTemplateOrLateResolvableType(); } - public function toTraversable(): Type - { - return new GenericObjectType(Traversable::class, [ - $this->keyType, - $this->itemType, - ]); - } - } diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index a5c903db730..1348e27dc5f 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -195,10 +195,7 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof IterableType) { - return $this->accepts(new UnionType([ - $type->toArray(), - $type->toTraversable(), - ]), $strictTypes); + return $this->accepts($type->toArrayOrTraversable(), $strictTypes); } foreach (self::EQUAL_UNION_CLASSES as $baseClass => $classes) { @@ -1081,10 +1078,7 @@ public function toCoercedArgumentType(bool $strictTypes): Type public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ($receivedType instanceof IterableType) { - $receivedType = new UnionType([ - $receivedType->toArray(), - $receivedType->toTraversable(), - ]); + $receivedType = $receivedType->toArrayOrTraversable(); } $types = TemplateTypeMap::createEmpty();