diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 8af74bf926..065f8ade8a 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -592,6 +592,8 @@ private function resolveUnionTypeNode(UnionTypeNode $typeNode, NameScope $nameSc $arrayTypeType = TypeCombinator::union(...$arrayTypeTypes); $addArray = true; + // Pass 1: merge non-array iterables (ObjectType, IterableType) + $nonArrayMerged = false; foreach ($otherTypeTypes as &$type) { if (!$type->isIterable()->yes() || !$type->getIterableValueType()->isSuperTypeOf($arrayTypeType)->yes()) { continue; @@ -599,18 +601,34 @@ private function resolveUnionTypeNode(UnionTypeNode $typeNode, NameScope $nameSc if ($type instanceof ObjectType && !$type instanceof GenericObjectType) { $type = new IntersectionType([$type, new IterableType(new MixedType(), $arrayTypeType)]); - } elseif ($type instanceof ArrayType) { - $type = new ArrayType(new MixedType(), $arrayTypeType); - } elseif ($type instanceof ConstantArrayType) { - $type = new ArrayType(new MixedType(), $arrayTypeType); + $nonArrayMerged = true; } elseif ($type instanceof IterableType) { $type = new IterableType(new MixedType(), $arrayTypeType); + $nonArrayMerged = true; } else { continue; } $addArray = false; } + unset($type); + + // Pass 2: merge array types only if non-array iterables were also merged + if ($nonArrayMerged) { + foreach ($otherTypeTypes as &$type) { + if (!$type->isIterable()->yes() || !$type->getIterableValueType()->isSuperTypeOf($arrayTypeType)->yes()) { + continue; + } + + if (!($type instanceof ArrayType) && !($type instanceof ConstantArrayType)) { + continue; + } + + $type = new ArrayType(new MixedType(), $arrayTypeType); + $addArray = false; + } + unset($type); + } if ($addArray) { $otherTypeTypes[] = new ArrayType(new MixedType(), $arrayTypeType); diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php index 7d43d80c8d..c72961b38b 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php @@ -33,6 +33,11 @@ public function testRule(): void 'Function MissingFunctionParameterTypehint\namespacedFunction() has parameter $d with no type specified.', 24, ], + [ + 'Function MissingFunctionParameterTypehint\intIterableTypehint() has parameter $a with no value type specified in iterable type array.', + 31, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], [ 'Function MissingFunctionParameterTypehint\missingArrayTypehint() has parameter $a with no value type specified in iterable type array.', 36, diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 99480a4061..43ced7b2cb 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3938,4 +3938,13 @@ public function testBug11463(): void ]); } + public function testBug3128(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-3128.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-3128.php b/tests/PHPStan/Rules/Methods/data/bug-3128.php new file mode 100644 index 0000000000..f1b7e4c2a5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3128.php @@ -0,0 +1,24 @@ +addTos([1]); +$a->addTosReversed([1]);