Skip to content

Commit fe427cd

Browse files
Add ArrayChangeKeyCaseFunctionReturnTypeExtension
1 parent ee802d6 commit fe427cd

File tree

5 files changed

+209
-2
lines changed

5 files changed

+209
-2
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,11 @@ services:
11601160
tags:
11611161
- phpstan.broker.dynamicFunctionReturnTypeExtension
11621162

1163+
-
1164+
class: PHPStan\Type\Php\ArrayChangeKeyCaseFunctionReturnTypeExtension
1165+
tags:
1166+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1167+
11631168
-
11641169
class: PHPStan\Type\Php\ArrayIntersectKeyFunctionReturnTypeExtension
11651170
tags:

src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ final class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionR
1818

1919
private const FUNCTION_NAMES = [
2020
'array_unique' => 0,
21-
'array_change_key_case' => 0,
2221
'array_diff_assoc' => 0,
2322
'array_diff_key' => 0,
2423
'array_diff_uassoc' => 0,
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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\Reflection\FunctionReflection;
8+
use PHPStan\Type\Accessory\AccessoryArrayListType;
9+
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
10+
use PHPStan\Type\Accessory\NonEmptyArrayType;
11+
use PHPStan\Type\ArrayType;
12+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
13+
use PHPStan\Type\Constant\ConstantStringType;
14+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
15+
use PHPStan\Type\StringType;
16+
use PHPStan\Type\Type;
17+
use PHPStan\Type\TypeCombinator;
18+
use PHPStan\Type\TypeTraverser;
19+
use PHPStan\Type\TypeUtils;
20+
use PHPStan\Type\UnionType;
21+
use function array_filter;
22+
use function count;
23+
use function strtolower;
24+
use function strtoupper;
25+
use const CASE_LOWER;
26+
27+
final class ArrayChangeKeyCaseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
28+
{
29+
30+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
31+
{
32+
return $functionReflection->getName() === 'array_change_key_case';
33+
}
34+
35+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
36+
{
37+
if (!isset($functionCall->getArgs()[0])) {
38+
return null;
39+
}
40+
41+
$arrayType = $scope->getType($functionCall->getArgs()[0]->value);
42+
if (!isset($functionCall->getArgs()[1])) {
43+
$case = CASE_LOWER;
44+
} else {
45+
$caseType = $scope->getType($functionCall->getArgs()[1]->value);
46+
$scalarValues = $caseType->getConstantScalarValues();
47+
if (count($scalarValues) === 1) {
48+
$case = $scalarValues[0];
49+
} else {
50+
$case = null;
51+
}
52+
}
53+
54+
$constantArrays = $arrayType->getConstantArrays();
55+
if (count($constantArrays) > 0) {
56+
$arrayTypes = [];
57+
foreach ($constantArrays as $constantArray) {
58+
$newConstantArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
59+
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
60+
$valueType = $constantArray->getOffsetValueType($keyType);
61+
if ($keyType->isString()->yes()) {
62+
if (!isset($case)) {
63+
$keyType = TypeCombinator::union(
64+
new ConstantStringType(strtolower((string) $keyType->getValue())),
65+
new ConstantStringType(strtoupper((string) $keyType->getValue())),
66+
);
67+
} elseif ($case === CASE_LOWER) {
68+
$keyType = new ConstantStringType(strtolower((string) $keyType->getValue()));
69+
} else {
70+
$keyType = new ConstantStringType(strtoupper((string) $keyType->getValue()));
71+
}
72+
}
73+
74+
$newConstantArrayBuilder->setOffsetValueType(
75+
$keyType,
76+
$valueType,
77+
$constantArray->isOptionalKey($i),
78+
);
79+
}
80+
$newConstantArrayType = $newConstantArrayBuilder->getArray();
81+
if ($constantArray->isList()->yes()) {
82+
$newConstantArrayType = AccessoryArrayListType::intersectWith($newConstantArrayType);
83+
}
84+
$arrayTypes[] = $newConstantArrayType;
85+
}
86+
87+
$newArrayType = TypeCombinator::union(...$arrayTypes);
88+
} else {
89+
$keysType = $arrayType->getIterableKeyType();
90+
91+
$keysType = TypeTraverser::map($keysType, static function (Type $type, callable $traverse) use ($case): Type {
92+
if ($type instanceof UnionType) {
93+
return $traverse($type);
94+
}
95+
96+
if ($type->isString()->yes()) {
97+
if ($case === CASE_LOWER) {
98+
return TypeCombinator::intersect($type, new AccessoryLowercaseStringType());
99+
} elseif ($type->isLowercaseString()->yes()) {
100+
return TypeCombinator::intersect(
101+
new StringType(),
102+
...array_filter(
103+
TypeUtils::getAccessoryTypes($type),
104+
static fn (Type $accessory): bool => !$accessory instanceof AccessoryLowercaseStringType,
105+
),
106+
);
107+
}
108+
}
109+
110+
return $type;
111+
});
112+
113+
$newArrayType = TypeCombinator::intersect(new ArrayType(
114+
$keysType,
115+
$arrayType->getIterableValueType(),
116+
), ...TypeUtils::getAccessoryTypes($arrayType));
117+
}
118+
119+
if ($arrayType->isIterableAtLeastOnce()->yes()) {
120+
$newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType());
121+
}
122+
123+
return $newArrayType;
124+
}
125+
126+
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4577,7 +4577,7 @@ public function dataArrayFunctions(): array
45774577
'$reducedToInt',
45784578
],
45794579
[
4580-
'array<0|1|2, 1|2|3>',
4580+
'array{1, 2, 3}',
45814581
'array_change_key_case($integers)',
45824582
],
45834583
[
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ArrayChangeKeyCase;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/**
10+
* @param array<string> $arr1
11+
* @param array<string, string> $arr2
12+
* @param array<string|int, string> $arr3
13+
* @param array<int, string> $arr4
14+
* @param array<lowercase-string, string> $arr5
15+
* @param array<lowercase-string&non-falsy-string, string> $arr6
16+
* @param array{foo: 1, bar?: 2} $arr7
17+
* @param list<string> $list
18+
* @param non-empty-array<string> $nonEmpty
19+
*/
20+
public function sayHello(
21+
array $arr1,
22+
array $arr2,
23+
array $arr3,
24+
array $arr4,
25+
array $arr5,
26+
array $arr6,
27+
array $arr7,
28+
array $list,
29+
array $nonEmpty,
30+
int $case
31+
): void {
32+
assertType('array<string>', array_change_key_case($arr1));
33+
assertType('array<string>', array_change_key_case($arr1, CASE_LOWER));
34+
assertType('array<string>', array_change_key_case($arr1, CASE_UPPER));
35+
assertType('array<string>', array_change_key_case($arr1, $case));
36+
37+
assertType('array<lowercase-string, string>', array_change_key_case($arr2));
38+
assertType('array<lowercase-string, string>', array_change_key_case($arr2, CASE_LOWER));
39+
assertType('array<string, string>', array_change_key_case($arr2, CASE_UPPER));
40+
assertType('array<string, string>', array_change_key_case($arr2, $case));
41+
42+
assertType('array<int|lowercase-string, string>', array_change_key_case($arr3));
43+
assertType('array<int|lowercase-string, string>', array_change_key_case($arr3, CASE_LOWER));
44+
assertType('array<int|string, string>', array_change_key_case($arr3, CASE_UPPER));
45+
assertType('array<int|string, string>', array_change_key_case($arr3, $case));
46+
47+
assertType('array<int, string>', array_change_key_case($arr4));
48+
assertType('array<int, string>', array_change_key_case($arr4, CASE_LOWER));
49+
assertType('array<int, string>', array_change_key_case($arr4, CASE_UPPER));
50+
assertType('array<int, string>', array_change_key_case($arr4, $case));
51+
52+
assertType('array<lowercase-string, string>', array_change_key_case($arr5));
53+
assertType('array<lowercase-string, string>', array_change_key_case($arr5, CASE_LOWER));
54+
assertType('array<string, string>', array_change_key_case($arr5, CASE_UPPER));
55+
assertType('array<string, string>', array_change_key_case($arr5, $case));
56+
57+
assertType('array<lowercase-string&non-falsy-string, string>', array_change_key_case($arr6));
58+
assertType('array<lowercase-string&non-falsy-string, string>', array_change_key_case($arr6, CASE_LOWER));
59+
assertType('array<non-falsy-string, string>', array_change_key_case($arr6, CASE_UPPER));
60+
assertType('array<non-falsy-string, string>', array_change_key_case($arr6, $case));
61+
62+
assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr7));
63+
assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr7, CASE_LOWER));
64+
assertType('array{FOO: 1, BAR?: 2}', array_change_key_case($arr7, CASE_UPPER));
65+
assertType("non-empty-array<'BAR'|'bar'|'FOO'|'foo', 1|2>", array_change_key_case($arr7, $case));
66+
67+
assertType('list<string>', array_change_key_case($list));
68+
assertType('list<string>', array_change_key_case($list, CASE_LOWER));
69+
assertType('list<string>', array_change_key_case($list, CASE_UPPER));
70+
assertType('list<string>', array_change_key_case($list, $case));
71+
72+
assertType('non-empty-array<string>', array_change_key_case($nonEmpty));
73+
assertType('non-empty-array<string>', array_change_key_case($nonEmpty, CASE_LOWER));
74+
assertType('non-empty-array<string>', array_change_key_case($nonEmpty, CASE_UPPER));
75+
assertType('non-empty-array<string>', array_change_key_case($nonEmpty, $case));
76+
}
77+
}

0 commit comments

Comments
 (0)