diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 051d15d960..395626e1ce 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2335,12 +2335,31 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu return new ErrorType(); } - return ParametersAcceptorSelector::selectFromArgs( + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, $node->getArgs(), $calledOnType->getCallableParametersAcceptors($this), null, - )->getReturnType(); + ); + + $functionName = null; + 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->name instanceof Name) { + $functionName = $node->name->name; + } + + if ($functionName !== null && $this->reflectionProvider->hasFunction($functionName, $this)) { + $functionReflection = $this->reflectionProvider->getFunction($functionName, $this); + $resolvedType = $this->getDynamicFunctionReturnType($parametersAcceptor, $node, $functionReflection); + if ($resolvedType !== null) { + return $resolvedType; + } + } + + return $parametersAcceptor->getReturnType(); } if (!$this->reflectionProvider->hasFunction($node->name, $this)) { @@ -2369,19 +2388,9 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu ); $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); if ($normalizedNode !== null) { - foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) { - if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { - continue; - } - - $resolvedType = $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall( - $functionReflection, - $normalizedNode, - $this, - ); - if ($resolvedType !== null) { - return $resolvedType; - } + $resolvedType = $this->getDynamicFunctionReturnType($parametersAcceptor, $normalizedNode, $functionReflection); + if ($resolvedType !== null) { + return $resolvedType; } } @@ -2391,6 +2400,29 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu return new MixedType(); } + private function getDynamicFunctionReturnType(ParametersAcceptor $parametersAcceptor, FuncCall $node, FunctionReflection $functionReflection): ?Type + { + $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + if ($normalizedNode !== null) { + foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) { + if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { + continue; + } + + $resolvedType = $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall( + $functionReflection, + $node, + $this, + ); + if ($resolvedType !== null) { + return $resolvedType; + } + } + } + + return null; + } + private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type { if ($expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) { 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'])); + } + +}