Skip to content

Commit 650df55

Browse files
committed
Improve the return type of array_map() for constant arrays
1 parent 4f7beff commit 650df55

File tree

2 files changed

+69
-1
lines changed

2 files changed

+69
-1
lines changed

src/Type/Php/ArrayMapFunctionReturnTypeExtension.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22

33
namespace PHPStan\Type\Php;
44

5+
use PhpParser\Node\Arg;
56
use PhpParser\Node\Expr\FuncCall;
7+
use PhpParser\Node\Name;
8+
use PhpParser\Node\Scalar\String_;
69
use PHPStan\Analyser\Scope;
10+
use PHPStan\Node\Expr\TypeExpr;
11+
use PHPStan\Parser\ArrayMapArgVisitor;
712
use PHPStan\Reflection\FunctionReflection;
813
use PHPStan\Type\Accessory\AccessoryArrayListType;
914
use PHPStan\Type\Accessory\NonEmptyArrayType;
1015
use PHPStan\Type\ArrayType;
16+
use PHPStan\Type\ClosureType;
1117
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1218
use PHPStan\Type\Constant\ConstantIntegerType;
19+
use PHPStan\Type\Constant\ConstantStringType;
1320
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1421
use PHPStan\Type\IntegerType;
1522
use PHPStan\Type\MixedType;
@@ -37,13 +44,27 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3744
$singleArrayArgument = !isset($functionCall->getArgs()[2]);
3845
$callableType = $scope->getType($functionCall->getArgs()[0]->value);
3946
$callableIsNull = $callableType->isNull()->yes();
47+
$callback = null;
4048

4149
if ($callableType->isCallable()->yes()) {
4250
$valueTypes = [new NeverType()];
4351
foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) {
4452
$valueTypes[] = $parametersAcceptor->getReturnType();
4553
}
4654
$valueType = TypeCombinator::union(...$valueTypes);
55+
$callback = $functionCall->getArgs()[0]->value;
56+
if ($callback instanceof String_) {
57+
/** @var non-falsy-string $callName */
58+
$callName = $callback->value;
59+
$callback = new Name($callName);
60+
} elseif ($callback instanceof FuncCall && $callback->isFirstClassCallable() &&
61+
$callback->getAttribute('phpstan_cache_printer') !== null &&
62+
preg_match('/\A(?<name>\\\\?[^()]+)\(...\)\z/', $callback->getAttribute('phpstan_cache_printer'), $m) === 1
63+
) {
64+
/** @var non-falsy-string $callName */
65+
$callName = $m['name'];
66+
$callback = new Name($callName);
67+
}
4768
} elseif ($callableIsNull) {
4869
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
4970
foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) {
@@ -73,7 +94,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
7394
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
7495
$returnedArrayBuilder->setOffsetValueType(
7596
$keyType,
76-
$valueType,
97+
$callback === null
98+
? $valueType
99+
: $scope->getType(new FuncCall($callback, [
100+
new Arg(new TypeExpr($constantArray->getValueTypes()[$i])),
101+
])),
77102
$constantArray->isOptionalKey($i),
78103
);
79104
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace ArrayMapCallable81;
4+
5+
use function PHPStan\Testing\assertType;
6+
use function strval as str;
7+
8+
class Foo
9+
{
10+
/**
11+
* @template T of int
12+
* @param T $n
13+
* @return (T is 3 ? 'Fizz' : (T is 5 ? 'Buzz' : T))
14+
*/
15+
public static function fizzbuzz(int $n): int|string
16+
{
17+
return match ($n) {
18+
3 => 'Fizz',
19+
5 => 'Buzz',
20+
default => $n,
21+
};
22+
}
23+
24+
public function doFoo(): void
25+
{
26+
$a = range(0, 1);
27+
28+
assertType("array{'0', '1'}", array_map('strval', $a));
29+
assertType("array{'0', '1'}", array_map(strval(...), $a));
30+
assertType("array{'0', '1'}", array_map(str(...), $a));
31+
assertType("array{'0'|'1', '0'|'1'}", array_map(fn ($v) => strval($v), $a));
32+
assertType("array{'0'|'1', '0'|'1'}", array_map(fn ($v) => (string)$v, $a));
33+
}
34+
35+
public function doFizzBuzz(): void
36+
{
37+
assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map([__CLASS__, 'fizzbuzz'], range(1, 6)));
38+
assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map([$this, 'fizzbuzz'], range(1, 6)));
39+
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)));
40+
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)));
41+
}
42+
43+
}

0 commit comments

Comments
 (0)