Skip to content

Commit f247414

Browse files
committed
Add ArrayCountValuesDynamicReturnTypeExtension.
1 parent 19dfc5e commit f247414

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\DependencyInjection\AutowiredService;
8+
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\Type\ArrayType;
10+
use PHPStan\Type\Constant\ConstantArrayType;
11+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12+
use PHPStan\Type\ErrorType;
13+
use PHPStan\Type\IntegerRangeType;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeCombinator;
16+
use PHPStan\Type\UnionType;
17+
use function count;
18+
19+
#[AutowiredService]
20+
final class ArrayCountValuesDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
21+
{
22+
23+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
24+
{
25+
return $functionReflection->getName() === 'array_count_values';
26+
}
27+
28+
public function getTypeFromFunctionCall(
29+
FunctionReflection $functionReflection,
30+
FuncCall $functionCall,
31+
Scope $scope,
32+
): ?Type
33+
{
34+
$args = $functionCall->getArgs();
35+
36+
if (!isset($args[0])) {
37+
return null;
38+
}
39+
40+
$inputType = $scope->getType($args[0]->value);
41+
42+
$arrayTypes = $inputType->getArrays();
43+
44+
$outputTypes = [];
45+
46+
foreach ($arrayTypes as $arrayType) {
47+
$itemType = $arrayType->getItemType();
48+
49+
if ($itemType instanceof UnionType) {
50+
$itemType = $itemType->filterTypes(
51+
static fn ($type) => !$type->toArrayKey() instanceof ErrorType,
52+
);
53+
}
54+
55+
if ($itemType->toArrayKey() instanceof ErrorType) {
56+
continue;
57+
}
58+
59+
$outputTypes[] = new ArrayType(
60+
$itemType,
61+
IntegerRangeType::fromInterval(1, null),
62+
);
63+
}
64+
65+
if (count($outputTypes) === 0) {
66+
return new ConstantArrayType([], []);
67+
}
68+
69+
return TypeCombinator::union(...$outputTypes);
70+
}
71+
72+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace ArrayCountValues;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
$ints = array_count_values([1, 2, 2, 3]);
8+
9+
assertType('array<1|2|3, int<1, max>>', $ints);
10+
11+
$strings = array_count_values(['one', 'two', 'two', 'three']);
12+
13+
assertType('array<\'one\'|\'three\'|\'two\', int<1, max>>', $strings);
14+
15+
$objects = array_count_values([new \stdClass()]);
16+
17+
assertType('array{}', $objects);
18+
19+
/**
20+
* @return array<int, string|object>
21+
*/
22+
function returnsStringOrObjectArray(): array
23+
{
24+
25+
}
26+
27+
// Objects are ignored by array_count_values, with a warning emitted.
28+
assertType('array<string, int<1, max>>', array_count_values(returnsStringOrObjectArray()));
29+
30+
class StringableObject
31+
{
32+
33+
public function __toString(): string
34+
{
35+
return 'string';
36+
}
37+
38+
}
39+
40+
// Stringable objects are ignored by array_count_values, with a warning emitted.
41+
$stringable = array_count_values([new StringableObject(), 'string', 1]);
42+
43+
assertType('array<1|\'string\', int<1, max>>', $stringable);

0 commit comments

Comments
 (0)