Skip to content

Commit 06507a5

Browse files
authored
Merge branch refs/heads/1.12.x into 2.1.x
2 parents 5a6f45e + b9894fa commit 06507a5

7 files changed

+296
-2
lines changed

conf/config.neon

+5
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,11 @@ services:
11111111
tags:
11121112
- phpstan.broker.dynamicFunctionReturnTypeExtension
11131113

1114+
-
1115+
class: PHPStan\Type\Php\ArrayChangeKeyCaseFunctionReturnTypeExtension
1116+
tags:
1117+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1118+
11141119
-
11151120
class: PHPStan\Type\Php\ArrayIntersectKeyFunctionReturnTypeExtension
11161121
tags:

src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php

-1
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,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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\AccessoryNonEmptyStringType;
11+
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
12+
use PHPStan\Type\Accessory\AccessoryNumericStringType;
13+
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
14+
use PHPStan\Type\Accessory\NonEmptyArrayType;
15+
use PHPStan\Type\ArrayType;
16+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
17+
use PHPStan\Type\Constant\ConstantStringType;
18+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
19+
use PHPStan\Type\StringType;
20+
use PHPStan\Type\Type;
21+
use PHPStan\Type\TypeCombinator;
22+
use PHPStan\Type\TypeTraverser;
23+
use PHPStan\Type\TypeUtils;
24+
use PHPStan\Type\UnionType;
25+
use function array_map;
26+
use function count;
27+
use function strtolower;
28+
use function strtoupper;
29+
use const CASE_LOWER;
30+
use const CASE_UPPER;
31+
32+
final class ArrayChangeKeyCaseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
33+
{
34+
35+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
36+
{
37+
return $functionReflection->getName() === 'array_change_key_case';
38+
}
39+
40+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
41+
{
42+
if (!isset($functionCall->getArgs()[0])) {
43+
return null;
44+
}
45+
46+
$arrayType = $scope->getType($functionCall->getArgs()[0]->value);
47+
if (!isset($functionCall->getArgs()[1])) {
48+
$case = CASE_LOWER;
49+
} else {
50+
$caseType = $scope->getType($functionCall->getArgs()[1]->value);
51+
$scalarValues = $caseType->getConstantScalarValues();
52+
if (count($scalarValues) === 1) {
53+
$case = (int) $scalarValues[0];
54+
} else {
55+
$case = null;
56+
}
57+
}
58+
59+
$constantArrays = $arrayType->getConstantArrays();
60+
if (count($constantArrays) > 0) {
61+
$arrayTypes = [];
62+
foreach ($constantArrays as $constantArray) {
63+
$newConstantArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
64+
$valueTypes = $constantArray->getValueTypes();
65+
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
66+
$valueType = $valueTypes[$i];
67+
68+
$constantStrings = $keyType->getConstantStrings();
69+
if (count($constantStrings) > 0) {
70+
$keyType = TypeCombinator::union(
71+
...array_map(
72+
fn (ConstantStringType $type): Type => $this->mapConstantString($type, $case),
73+
$constantStrings,
74+
),
75+
);
76+
}
77+
78+
$newConstantArrayBuilder->setOffsetValueType(
79+
$keyType,
80+
$valueType,
81+
$constantArray->isOptionalKey($i),
82+
);
83+
}
84+
$newConstantArrayType = $newConstantArrayBuilder->getArray();
85+
if ($constantArray->isList()->yes()) {
86+
$newConstantArrayType = AccessoryArrayListType::intersectWith($newConstantArrayType);
87+
}
88+
$arrayTypes[] = $newConstantArrayType;
89+
}
90+
91+
$newArrayType = TypeCombinator::union(...$arrayTypes);
92+
} else {
93+
$keysType = $arrayType->getIterableKeyType();
94+
95+
$keysType = TypeTraverser::map($keysType, function (Type $type, callable $traverse) use ($case): Type {
96+
if ($type instanceof UnionType) {
97+
return $traverse($type);
98+
}
99+
100+
$constantStrings = $type->getConstantStrings();
101+
if (count($constantStrings) > 0) {
102+
return TypeCombinator::union(
103+
...array_map(
104+
fn (ConstantStringType $type): Type => $this->mapConstantString($type, $case),
105+
$constantStrings,
106+
),
107+
);
108+
}
109+
110+
if ($type->isString()->yes()) {
111+
$types = [new StringType()];
112+
if ($type->isNonFalsyString()->yes()) {
113+
$types[] = new AccessoryNonFalsyStringType();
114+
} elseif ($type->isNonEmptyString()->yes()) {
115+
$types[] = new AccessoryNonEmptyStringType();
116+
}
117+
if ($type->isNumericString()->yes()) {
118+
$types[] = new AccessoryNumericStringType();
119+
}
120+
if ($case === CASE_LOWER) {
121+
$types[] = new AccessoryLowercaseStringType();
122+
} elseif ($case === CASE_UPPER) {
123+
$types[] = new AccessoryUppercaseStringType();
124+
}
125+
126+
return TypeCombinator::intersect(...$types);
127+
}
128+
129+
return $type;
130+
});
131+
132+
$newArrayType = TypeCombinator::intersect(new ArrayType(
133+
$keysType,
134+
$arrayType->getIterableValueType(),
135+
), ...TypeUtils::getAccessoryTypes($arrayType));
136+
}
137+
138+
if ($arrayType->isIterableAtLeastOnce()->yes()) {
139+
$newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType());
140+
}
141+
142+
return $newArrayType;
143+
}
144+
145+
private function mapConstantString(ConstantStringType $type, ?int $case): Type
146+
{
147+
if ($case === CASE_LOWER) {
148+
return new ConstantStringType(strtolower($type->getValue()));
149+
} elseif ($case === CASE_UPPER) {
150+
return new ConstantStringType(strtoupper($type->getValue()));
151+
}
152+
153+
return TypeCombinator::union(
154+
new ConstantStringType(strtolower($type->getValue())),
155+
new ConstantStringType(strtoupper($type->getValue())),
156+
);
157+
}
158+
159+
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

+1-1
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
[
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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<non-empty-string, string> $arr7
17+
* @param array<literal-string, string> $arr8
18+
* @param array{foo: 1, bar?: 2} $arr9
19+
* @param array<'foo'|'bar', string> $arr10
20+
* @param list<string> $list
21+
* @param non-empty-array<string> $nonEmpty
22+
*/
23+
public function sayHello(
24+
array $arr1,
25+
array $arr2,
26+
array $arr3,
27+
array $arr4,
28+
array $arr5,
29+
array $arr6,
30+
array $arr7,
31+
array $arr8,
32+
array $arr9,
33+
array $arr10,
34+
array $list,
35+
array $nonEmpty,
36+
int $case
37+
): void {
38+
assertType('array<string>', array_change_key_case($arr1));
39+
assertType('array<string>', array_change_key_case($arr1, CASE_LOWER));
40+
assertType('array<string>', array_change_key_case($arr1, CASE_UPPER));
41+
assertType('array<string>', array_change_key_case($arr1, $case));
42+
43+
assertType('array<lowercase-string, string>', array_change_key_case($arr2));
44+
assertType('array<lowercase-string, string>', array_change_key_case($arr2, CASE_LOWER));
45+
assertType('array<uppercase-string, string>', array_change_key_case($arr2, CASE_UPPER));
46+
assertType('array<string, string>', array_change_key_case($arr2, $case));
47+
48+
assertType('array<int|lowercase-string, string>', array_change_key_case($arr3));
49+
assertType('array<int|lowercase-string, string>', array_change_key_case($arr3, CASE_LOWER));
50+
assertType('array<int|uppercase-string, string>', array_change_key_case($arr3, CASE_UPPER));
51+
assertType('array<int|string, string>', array_change_key_case($arr3, $case));
52+
53+
assertType('array<int, string>', array_change_key_case($arr4));
54+
assertType('array<int, string>', array_change_key_case($arr4, CASE_LOWER));
55+
assertType('array<int, string>', array_change_key_case($arr4, CASE_UPPER));
56+
assertType('array<int, string>', array_change_key_case($arr4, $case));
57+
58+
assertType('array<lowercase-string, string>', array_change_key_case($arr5));
59+
assertType('array<lowercase-string, string>', array_change_key_case($arr5, CASE_LOWER));
60+
assertType('array<uppercase-string, string>', array_change_key_case($arr5, CASE_UPPER));
61+
assertType('array<string, string>', array_change_key_case($arr5, $case));
62+
63+
assertType('array<lowercase-string&non-falsy-string, string>', array_change_key_case($arr6));
64+
assertType('array<lowercase-string&non-falsy-string, string>', array_change_key_case($arr6, CASE_LOWER));
65+
assertType('array<non-falsy-string&uppercase-string, string>', array_change_key_case($arr6, CASE_UPPER));
66+
assertType('array<non-falsy-string, string>', array_change_key_case($arr6, $case));
67+
68+
assertType('array<lowercase-string&non-empty-string, string>', array_change_key_case($arr7));
69+
assertType('array<lowercase-string&non-empty-string, string>', array_change_key_case($arr7, CASE_LOWER));
70+
assertType('array<non-empty-string&uppercase-string, string>', array_change_key_case($arr7, CASE_UPPER));
71+
assertType('array<non-empty-string, string>', array_change_key_case($arr7, $case));
72+
73+
assertType('array<lowercase-string, string>', array_change_key_case($arr8));
74+
assertType('array<lowercase-string, string>', array_change_key_case($arr8, CASE_LOWER));
75+
assertType('array<uppercase-string, string>', array_change_key_case($arr8, CASE_UPPER));
76+
assertType('array<string, string>', array_change_key_case($arr8, $case));
77+
78+
assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr9));
79+
assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr9, CASE_LOWER));
80+
assertType('array{FOO: 1, BAR?: 2}', array_change_key_case($arr9, CASE_UPPER));
81+
assertType("non-empty-array<'BAR'|'bar'|'FOO'|'foo', 1|2>", array_change_key_case($arr9, $case));
82+
83+
assertType("array<'bar'|'foo', string>", array_change_key_case($arr10));
84+
assertType("array<'bar'|'foo', string>", array_change_key_case($arr10, CASE_LOWER));
85+
assertType("array<'BAR'|'FOO', string>", array_change_key_case($arr10, CASE_UPPER));
86+
assertType("array<'BAR'|'bar'|'FOO'|'foo', string>", array_change_key_case($arr10, $case));
87+
88+
assertType('list<string>', array_change_key_case($list));
89+
assertType('list<string>', array_change_key_case($list, CASE_LOWER));
90+
assertType('list<string>', array_change_key_case($list, CASE_UPPER));
91+
assertType('list<string>', array_change_key_case($list, $case));
92+
93+
assertType('non-empty-array<string>', array_change_key_case($nonEmpty));
94+
assertType('non-empty-array<string>', array_change_key_case($nonEmpty, CASE_LOWER));
95+
assertType('non-empty-array<string>', array_change_key_case($nonEmpty, CASE_UPPER));
96+
assertType('non-empty-array<string>', array_change_key_case($nonEmpty, $case));
97+
}
98+
}

tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php

+7
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,13 @@ public function testBug10732(): void
287287
$this->analyse([__DIR__ . '/data/bug-10732.php'], []);
288288
}
289289

290+
public function testBug10960(): void
291+
{
292+
$this->checkExplicitMixed = true;
293+
$this->checkNullables = true;
294+
$this->analyse([__DIR__ . '/data/bug-10960.php'], []);
295+
}
296+
290297
public function testBug11518(): void
291298
{
292299
$this->checkExplicitMixed = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug10960;
4+
5+
/**
6+
* @param array{foo: string} $foo
7+
*
8+
* @return array{FOO: string}
9+
*/
10+
function upperCaseKey(array $foo): array
11+
{
12+
return array_change_key_case($foo, CASE_UPPER);
13+
}
14+
15+
/**
16+
* @param array{FOO: string} $foo
17+
*
18+
* @return array{foo: string}
19+
*/
20+
function lowerCaseKey(array $foo): array
21+
{
22+
return array_change_key_case($foo, CASE_LOWER);
23+
}
24+
25+
upperCaseKey(['foo' => 'bar']);
26+
lowerCaseKey(['FOO' => 'bar']);

0 commit comments

Comments
 (0)