diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 24561b5598..e8e9e3a457 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -2,9 +2,11 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -13,10 +15,10 @@ use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_map; use function array_slice; use function count; @@ -38,12 +40,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $callableType = $scope->getType($functionCall->getArgs()[0]->value); $callableIsNull = $callableType->isNull()->yes(); + $callableParametersAcceptors = null; + if ($callableType->isCallable()->yes()) { - $valueTypes = [new NeverType()]; - foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) { - $valueTypes[] = $parametersAcceptor->getReturnType(); - } - $valueType = TypeCombinator::union(...$valueTypes); + $callableParametersAcceptors = $callableType->getCallableParametersAcceptors($scope); + $valueType = ParametersAcceptorSelector::selectFromTypes( + array_map( + static fn (Node\Arg $arg) => $scope->getType($arg->value)->getIterableValueType(), + array_slice($functionCall->getArgs(), 1), + ), + $callableParametersAcceptors, + false, + )->getReturnType(); } elseif ($callableIsNull) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) { @@ -70,10 +78,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($totalCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { foreach ($constantArrays as $constantArray) { $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $valueTypes = $constantArray->getValueTypes(); foreach ($constantArray->getKeyTypes() as $i => $keyType) { $returnedArrayBuilder->setOffsetValueType( $keyType, - $valueType, + $callableParametersAcceptors !== null + ? ParametersAcceptorSelector::selectFromTypes( + [$valueTypes[$i]], + $callableParametersAcceptors, + false, + )->getReturnType() + : $valueType, $constantArray->isOptionalKey($i), ); } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 2af1147c65..6d374a6f1c 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1049,4 +1049,9 @@ public function testBug11337(): void $this->analyse([__DIR__ . '/data/bug-11337.php'], []); } + public function testBug10715(): void + { + $this->analyse([__DIR__ . '/data/bug-10715.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-10715.php b/tests/PHPStan/Rules/Methods/data/bug-10715.php new file mode 100644 index 0000000000..82cd7f66ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10715.php @@ -0,0 +1,30 @@ + $word + * + * @return ($word is array ? array : string) + */ + public static function wgtrim(string|array $word): string|array + { + if (\is_array($word)) { + return array_map(static::wgtrim(...), $word); + } + + return 'word'; + } + + /** + * @param array{foo: array, bar: string} $array + * + * @return array{foo: array, bar: string} + */ + public static function example(array $array): array + { + return array_map(static::wgtrim(...), $array); + } +}