diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index f10fc22508..424319e7f9 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -150,6 +150,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; @@ -2224,25 +2225,45 @@ 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 ($functionName === null) { + throw new ShouldNotHappenException(); } - 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 24561b5598..404cf14996 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -2,8 +2,12 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -37,6 +41,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $singleArrayArgument = !isset($functionCall->getArgs()[2]); $callableType = $scope->getType($functionCall->getArgs()[0]->value); $callableIsNull = $callableType->isNull()->yes(); + $callback = null; if ($callableType->isCallable()->yes()) { $valueTypes = [new NeverType()]; @@ -44,6 +49,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $valueTypes[] = $parametersAcceptor->getReturnType(); } $valueType = TypeCombinator::union(...$valueTypes); + $callback = $functionCall->getArgs()[0]->value; } elseif ($callableIsNull) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) { @@ -73,7 +79,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, foreach ($constantArray->getKeyTypes() as $i => $keyType) { $returnedArrayBuilder->setOffsetValueType( $keyType, - $valueType, + $callback === null + ? $valueType + : $scope->getType(new FuncCall($callback, [ + new Arg(new TypeExpr($constantArray->getValueTypes()[$i])), + ])), $constantArray->isOptionalKey($i), ); } diff --git a/tests/PHPStan/Analyser/nsrt/array-map-callable81.php b/tests/PHPStan/Analyser/nsrt/array-map-callable81.php new file mode 100644 index 0000000000..ae5270ff2f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-map-callable81.php @@ -0,0 +1,43 @@ + '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'}", array_map(str(...), $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|'Buzz'|'Fizz', 2|'Buzz'|'Fizz', 3|'Buzz'|'Fizz', 4|'Buzz'|'Fizz', 5|'Buzz'|'Fizz', 6|'Buzz'|'Fizz'}", array_map(self::fizzbuzz(...), range(1, 6))); + assertType("array{1|'Buzz'|'Fizz', 2|'Buzz'|'Fizz', 3|'Buzz'|'Fizz', 4|'Buzz'|'Fizz', 5|'Buzz'|'Fizz', 6|'Buzz'|'Fizz'}", array_map($this->fizzbuzz(...), range(1, 6))); + } + +}