Skip to content

Commit 4d834fd

Browse files
authored
Add ArrayCountValuesDynamicReturnTypeExtension
1 parent cf57810 commit 4d834fd

File tree

2 files changed

+116
-0
lines changed

2 files changed

+116
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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\Accessory\NonEmptyArrayType;
10+
use PHPStan\Type\ArrayType;
11+
use PHPStan\Type\Constant\ConstantArrayType;
12+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
13+
use PHPStan\Type\ErrorType;
14+
use PHPStan\Type\IntegerRangeType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeCombinator;
17+
use PHPStan\Type\UnionType;
18+
use function count;
19+
20+
#[AutowiredService]
21+
final class ArrayCountValuesDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
22+
{
23+
24+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
25+
{
26+
return $functionReflection->getName() === 'array_count_values';
27+
}
28+
29+
public function getTypeFromFunctionCall(
30+
FunctionReflection $functionReflection,
31+
FuncCall $functionCall,
32+
Scope $scope,
33+
): ?Type
34+
{
35+
$args = $functionCall->getArgs();
36+
37+
if (!isset($args[0])) {
38+
return null;
39+
}
40+
41+
$inputType = $scope->getType($args[0]->value);
42+
43+
$arrayTypes = $inputType->getArrays();
44+
45+
$outputTypes = [];
46+
47+
foreach ($arrayTypes as $arrayType) {
48+
$itemType = $arrayType->getItemType();
49+
50+
if ($itemType instanceof UnionType) {
51+
$itemType = $itemType->filterTypes(
52+
static fn ($type) => !$type->toArrayKey() instanceof ErrorType,
53+
);
54+
}
55+
56+
if ($itemType->toArrayKey() instanceof ErrorType) {
57+
continue;
58+
}
59+
60+
$outputTypes[] = TypeCombinator::intersect(
61+
new ArrayType($itemType, IntegerRangeType::fromInterval(1, null)),
62+
new NonEmptyArrayType(),
63+
);
64+
}
65+
66+
if (count($outputTypes) === 0) {
67+
return new ConstantArrayType([], []);
68+
}
69+
70+
return TypeCombinator::union(...$outputTypes);
71+
}
72+
73+
}
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('non-empty-array<1|2|3, int<1, max>>', $ints);
10+
11+
$strings = array_count_values(['one', 'two', 'two', 'three']);
12+
13+
assertType('non-empty-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('non-empty-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('non-empty-array<1|\'string\', int<1, max>>', $stringable);

0 commit comments

Comments
 (0)