From 1e133b3db6393e45556f799b53701f4c9a3141c2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 21 Jun 2025 01:12:17 +0200 Subject: [PATCH] Improve return type of array map --- src/Analyser/MutatingScope.php | 37 +++++++++++---- .../ArrayMapFunctionReturnTypeExtension.php | 27 +++++------ tests/PHPStan/Analyser/nsrt/array-map.php | 45 +++++++++++++++++++ 3 files changed, 83 insertions(+), 26 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 051d15d960..da487fdbbc 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -159,6 +159,7 @@ use function is_string; use function ltrim; use function md5; +use function preg_match; use function sprintf; use function str_starts_with; use function strlen; @@ -2329,25 +2330,43 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu } if ($node instanceof FuncCall) { - if ($node->name instanceof Expr) { + $functionName = null; + if ($node->name instanceof Name) { + $functionName = $node->name; + } elseif ($node->name instanceof Expr) { $calledOnType = $this->getType($node->name); if ($calledOnType->isCallable()->no()) { return new ErrorType(); } - return ParametersAcceptorSelector::selectFromArgs( - $this, - $node->getArgs(), - $calledOnType->getCallableParametersAcceptors($this), - null, - )->getReturnType(); + if ($node->name instanceof String_) { + /** @var non-empty-string $name */ + $name = $node->name->value; + $functionName = new Name($name); + } elseif ( + $node->name instanceof FuncCall + && $node->name->isFirstClassCallable() + && $node->name->getAttribute('phpstan_cache_printer') !== null + && preg_match('/\A(?\\\\?[^()]+)\(...\)\z/', $node->name->getAttribute('phpstan_cache_printer'), $m) === 1 + ) { + /** @var non-falsy-string $name */ + $name = $m['name']; + $functionName = new Name($name); + } else { + return ParametersAcceptorSelector::selectFromArgs( + $this, + $node->getArgs(), + $calledOnType->getCallableParametersAcceptors($this), + null, + )->getReturnType(); + } } - if (!$this->reflectionProvider->hasFunction($node->name, $this)) { + if (!$this->reflectionProvider->hasFunction($functionName, $this)) { return new ErrorType(); } - $functionReflection = $this->reflectionProvider->getFunction($node->name, $this); + $functionReflection = $this->reflectionProvider->getFunction($functionName, $this); if ($this->nativeTypesPromoted) { return ParametersAcceptorSelector::combineAcceptors($functionReflection->getVariants())->getNativeReturnType(); } diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 591d9c8bde..a2831eeb4b 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -6,8 +6,8 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -41,21 +41,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $singleArrayArgument = !isset($functionCall->getArgs()[2]); - $callableType = $scope->getType($functionCall->getArgs()[0]->value); + $callback = $functionCall->getArgs()[0]->value; + $callableType = $scope->getType($callback); $callableIsNull = $callableType->isNull()->yes(); - $callableParametersAcceptors = null; - if ($callableType->isCallable()->yes()) { - $callableParametersAcceptors = $callableType->getCallableParametersAcceptors($scope); - $valueType = ParametersAcceptorSelector::selectFromTypes( + $valueType = $scope->getType(new FuncCall( + $callback, array_map( - static fn (Node\Arg $arg) => $scope->getType($arg->value)->getIterableValueType(), + static fn (Node\Arg $arg) => new Node\Arg(new TypeExpr($scope->getType($arg->value)->getIterableValueType())), array_slice($functionCall->getArgs(), 1), ), - $callableParametersAcceptors, - false, - )->getReturnType(); + )); } elseif ($callableIsNull) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); $argTypes = []; @@ -134,13 +131,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, foreach ($constantArray->getKeyTypes() as $i => $keyType) { $returnedArrayBuilder->setOffsetValueType( $keyType, - $callableParametersAcceptors !== null - ? ParametersAcceptorSelector::selectFromTypes( - [$valueTypes[$i]], - $callableParametersAcceptors, - false, - )->getReturnType() - : $valueType, + $scope->getType(new FuncCall($callback, [ + new Node\Arg(new TypeExpr($valueTypes[$i])), + ])), $constantArray->isOptionalKey($i), ); } diff --git a/tests/PHPStan/Analyser/nsrt/array-map.php b/tests/PHPStan/Analyser/nsrt/array-map.php index 75a68ab490..5dbafb1390 100644 --- a/tests/PHPStan/Analyser/nsrt/array-map.php +++ b/tests/PHPStan/Analyser/nsrt/array-map.php @@ -72,3 +72,48 @@ static function(string $string): string { assertType('array{foo?: string, bar?: string, baz?: string}', $mapped); } + +class Foo +{ + /** + * @template T of int + * @param T $n + * @return (T is 3 ? 'Fizz' : (T is 5 ? 'Buzz' : T)) + */ + public static function fizzbuzz(int $n): int|string + { + return match ($n) { + 3 => 'Fizz', + 5 => 'Buzz', + default => $n, + }; + } + + public function doFoo(): void + { + $a = range(0, 1); + + assertType("array{'0', '1'}", array_map('strval', $a)); + assertType("array{'0', '1'}", array_map(strval(...), $a)); + assertType("array{'0'|'1', '0'|'1'}", array_map(fn ($v) => strval($v), $a)); + assertType("array{'0'|'1', '0'|'1'}", array_map(fn ($v) => (string)$v, $a)); + } + + public function doFizzBuzz(): void + { + assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map([__CLASS__, 'fizzbuzz'], range(1, 6))); + assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map([$this, 'fizzbuzz'], range(1, 6))); + assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map(self::fizzbuzz(...), range(1, 6))); + assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map($this->fizzbuzz(...), range(1, 6))); + } + + /** + * @param array $array + */ + public function doUppercase(array $array): void + { + assertType("array", array_map(strtoupper(...), $array)); + assertType("array{'A', 'B'}", array_map(strtoupper(...), ['A', 'B'])); + } + +}