Skip to content

Commit 2e49201

Browse files
Improve return type of array map
1 parent c0f2e9e commit 2e49201

File tree

3 files changed

+81
-25
lines changed

3 files changed

+81
-25
lines changed

src/Analyser/MutatingScope.php

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2329,25 +2329,43 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
23292329
}
23302330

23312331
if ($node instanceof FuncCall) {
2332-
if ($node->name instanceof Expr) {
2332+
$functionName = null;
2333+
if ($node->name instanceof Name) {
2334+
$functionName = $node->name;
2335+
} elseif ($node->name instanceof Expr) {
23332336
$calledOnType = $this->getType($node->name);
23342337
if ($calledOnType->isCallable()->no()) {
23352338
return new ErrorType();
23362339
}
23372340

2338-
return ParametersAcceptorSelector::selectFromArgs(
2339-
$this,
2340-
$node->getArgs(),
2341-
$calledOnType->getCallableParametersAcceptors($this),
2342-
null,
2343-
)->getReturnType();
2341+
if ($node->name instanceof String_) {
2342+
/** @var non-empty-string $name */
2343+
$name = $node->name->value;
2344+
$functionName = new Name($name);
2345+
} elseif (
2346+
$node->name instanceof FuncCall
2347+
&& $node->name->isFirstClassCallable()
2348+
&& $node->name->getAttribute('phpstan_cache_printer') !== null
2349+
&& preg_match('/\A(?<name>\\\\?[^()]+)\(...\)\z/', $node->name->getAttribute('phpstan_cache_printer'), $m) === 1
2350+
) {
2351+
/** @var non-falsy-string $name */
2352+
$name = $m['name'];
2353+
$functionName = new Name($name);
2354+
} else {
2355+
return ParametersAcceptorSelector::selectFromArgs(
2356+
$this,
2357+
$node->getArgs(),
2358+
$calledOnType->getCallableParametersAcceptors($this),
2359+
null,
2360+
)->getReturnType();
2361+
}
23442362
}
23452363

2346-
if (!$this->reflectionProvider->hasFunction($node->name, $this)) {
2364+
if (!$this->reflectionProvider->hasFunction($functionName, $this)) {
23472365
return new ErrorType();
23482366
}
23492367

2350-
$functionReflection = $this->reflectionProvider->getFunction($node->name, $this);
2368+
$functionReflection = $this->reflectionProvider->getFunction($functionName, $this);
23512369
if ($this->nativeTypesPromoted) {
23522370
return ParametersAcceptorSelector::combineAcceptors($functionReflection->getVariants())->getNativeReturnType();
23532371
}

src/Type/Php/ArrayMapFunctionReturnTypeExtension.php

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpParser\Node\Expr\FuncCall;
77
use PHPStan\Analyser\Scope;
88
use PHPStan\DependencyInjection\AutowiredService;
9+
use PHPStan\Node\Expr\TypeExpr;
910
use PHPStan\Reflection\FunctionReflection;
1011
use PHPStan\Reflection\ParametersAcceptorSelector;
1112
use PHPStan\Type\Accessory\AccessoryArrayListType;
@@ -41,21 +42,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
4142
}
4243

4344
$singleArrayArgument = !isset($functionCall->getArgs()[2]);
44-
$callableType = $scope->getType($functionCall->getArgs()[0]->value);
45+
$callback = $functionCall->getArgs()[0]->value;
46+
$callableType = $scope->getType($callback);
4547
$callableIsNull = $callableType->isNull()->yes();
4648

47-
$callableParametersAcceptors = null;
48-
4949
if ($callableType->isCallable()->yes()) {
50-
$callableParametersAcceptors = $callableType->getCallableParametersAcceptors($scope);
51-
$valueType = ParametersAcceptorSelector::selectFromTypes(
50+
$valueType = $scope->getType(new FuncCall($callback,
5251
array_map(
53-
static fn (Node\Arg $arg) => $scope->getType($arg->value)->getIterableValueType(),
52+
static fn (Node\Arg $arg) => new Node\Arg(new TypeExpr($scope->getType($arg->value)->getIterableValueType())),
5453
array_slice($functionCall->getArgs(), 1),
5554
),
56-
$callableParametersAcceptors,
57-
false,
58-
)->getReturnType();
55+
));
5956
} elseif ($callableIsNull) {
6057
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
6158
$argTypes = [];
@@ -134,13 +131,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
134131
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
135132
$returnedArrayBuilder->setOffsetValueType(
136133
$keyType,
137-
$callableParametersAcceptors !== null
138-
? ParametersAcceptorSelector::selectFromTypes(
139-
[$valueTypes[$i]],
140-
$callableParametersAcceptors,
141-
false,
142-
)->getReturnType()
143-
: $valueType,
134+
$scope->getType(new FuncCall($callback, [
135+
new Node\Arg(new TypeExpr($valueTypes[$i])),
136+
])),
144137
$constantArray->isOptionalKey($i),
145138
);
146139
}

tests/PHPStan/Analyser/nsrt/array-map.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,48 @@ static function(string $string): string {
7272

7373
assertType('array{foo?: string, bar?: string, baz?: string}', $mapped);
7474
}
75+
76+
class Foo
77+
{
78+
/**
79+
* @template T of int
80+
* @param T $n
81+
* @return (T is 3 ? 'Fizz' : (T is 5 ? 'Buzz' : T))
82+
*/
83+
public static function fizzbuzz(int $n): int|string
84+
{
85+
return match ($n) {
86+
3 => 'Fizz',
87+
5 => 'Buzz',
88+
default => $n,
89+
};
90+
}
91+
92+
public function doFoo(): void
93+
{
94+
$a = range(0, 1);
95+
96+
assertType("array{'0', '1'}", array_map('strval', $a));
97+
assertType("array{'0', '1'}", array_map(strval(...), $a));
98+
assertType("array{'0'|'1', '0'|'1'}", array_map(fn ($v) => strval($v), $a));
99+
assertType("array{'0'|'1', '0'|'1'}", array_map(fn ($v) => (string)$v, $a));
100+
}
101+
102+
public function doFizzBuzz(): void
103+
{
104+
assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map([__CLASS__, 'fizzbuzz'], range(1, 6)));
105+
assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map([$this, 'fizzbuzz'], range(1, 6)));
106+
assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map(self::fizzbuzz(...), range(1, 6)));
107+
assertType("array{1, 2, 'Fizz', 4, 'Buzz', 6}", array_map($this->fizzbuzz(...), range(1, 6)));
108+
}
109+
110+
/**
111+
* @param array<string, 'a'|'b'|'A'|'B'> $array
112+
*/
113+
public function doUppercase(array $array): void
114+
{
115+
assertType("array<string, 'A'|'B'>", array_map(strtoupper(...), $array));
116+
assertType("array{'A', 'B'}", array_map(strtoupper(...), ['A', 'B']));
117+
}
118+
119+
}

0 commit comments

Comments
 (0)