Skip to content

Commit 3bd1bac

Browse files
canvuralondrejmirtes
authored andcommitted
Improve return types for array_fill_keys and array_combine
1 parent dc14e4a commit 3bd1bac

File tree

6 files changed

+257
-3
lines changed

6 files changed

+257
-3
lines changed

src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
use PHPStan\Type\Constant\ConstantIntegerType;
1616
use PHPStan\Type\Constant\ConstantStringType;
1717
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
18+
use PHPStan\Type\ErrorType;
19+
use PHPStan\Type\IntegerType;
1820
use PHPStan\Type\MixedType;
21+
use PHPStan\Type\NeverType;
1922
use PHPStan\Type\Type;
2023
use PHPStan\Type\TypeCombinator;
2124
use PHPStan\Type\UnionType;
@@ -65,9 +68,25 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
6568
}
6669
}
6770

71+
if ($keysParamType->isArray()->yes()) {
72+
$itemType = $keysParamType->getIterableValueType();
73+
74+
if ((new IntegerType())->isSuperTypeOf($itemType)->no()) {
75+
if ($itemType->toString() instanceof ErrorType) {
76+
return new NeverType();
77+
}
78+
79+
$keyType = $itemType->toString();
80+
} else {
81+
$keyType = $itemType;
82+
}
83+
} else {
84+
$keyType = new MixedType();
85+
}
86+
6887
$arrayType = new ArrayType(
69-
$keysParamType instanceof ArrayType ? $keysParamType->getItemType() : new MixedType(),
70-
$valuesParamType instanceof ArrayType ? $valuesParamType->getItemType() : new MixedType(),
88+
$keyType,
89+
$valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(),
7190
);
7291

7392
if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) {
@@ -95,6 +114,10 @@ private function sanitizeConstantArrayKeyTypes(array $types): ?array
95114
$sanitizedTypes = [];
96115

97116
foreach ($types as $type) {
117+
if ((new IntegerType())->isSuperTypeOf($type)->no() && ! $type->toString() instanceof ErrorType) {
118+
$type = $type->toString();
119+
}
120+
98121
if (
99122
!$type instanceof ConstantIntegerType
100123
&& !$type instanceof ConstantStringType

src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
use PHPStan\Type\ArrayType;
1010
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1111
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12+
use PHPStan\Type\ErrorType;
13+
use PHPStan\Type\IntegerType;
14+
use PHPStan\Type\NeverType;
1215
use PHPStan\Type\Type;
1316
use PHPStan\Type\TypeCombinator;
1417
use PHPStan\Type\TypeUtils;
@@ -39,7 +42,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3942
foreach ($constantArrays as $constantArray) {
4043
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
4144
foreach ($constantArray->getValueTypes() as $keyType) {
42-
$arrayBuilder->setOffsetValueType($keyType, $valueType);
45+
if ((new IntegerType())->isSuperTypeOf($keyType)->no()) {
46+
if ($keyType->toString() instanceof ErrorType) {
47+
return new NeverType();
48+
}
49+
50+
$arrayBuilder->setOffsetValueType($keyType->toString(), $valueType);
51+
} else {
52+
$arrayBuilder->setOffsetValueType($keyType, $valueType);
53+
}
4354
}
4455
$arrayTypes[] = $arrayBuilder->getArray();
4556
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,14 @@ public function dataFileAsserts(): iterable
829829
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6904.php');
830830
}
831831

832+
if (PHP_VERSION_ID >= 80000) {
833+
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-combine-php8.php');
834+
} else {
835+
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-combine-php7.php');
836+
}
837+
838+
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-fill-keys.php');
839+
832840
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6917.php');
833841
}
834842

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace ArrayCombinePHP7;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
/** @phpstan-return 'foo' */
10+
public function __toString(): string
11+
{
12+
return 'foo';
13+
}
14+
}
15+
16+
class Bar
17+
{
18+
public function __toString(): string
19+
{
20+
return 'bar';
21+
}
22+
}
23+
24+
class Baz {}
25+
26+
function withBoolKey(): void
27+
{
28+
$a = [true, 'red', 'yellow'];
29+
$b = ['avocado', 'apple', 'banana'];
30+
31+
assertType("array{1: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));
32+
33+
$c = [false, 'red', 'yellow'];
34+
$d = ['avocado', 'apple', 'banana'];
35+
36+
assertType("array{: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($c, $d));
37+
}
38+
39+
function withFloatKey(): void
40+
{
41+
$a = [1.5, 'red', 'yellow'];
42+
$b = ['avocado', 'apple', 'banana'];
43+
44+
assertType("array{1.5: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));
45+
}
46+
47+
function withIntegerKey(): void
48+
{
49+
$a = [1, 2, 3];
50+
$b = ['avocado', 'apple', 'banana'];
51+
52+
assertType("array{1: 'avocado', 2: 'apple', 3: 'banana'}", array_combine($a, $b));
53+
}
54+
55+
function withNumericStringKey(): void
56+
{
57+
$a = ["1", "2", "3"];
58+
$b = ['avocado', 'apple', 'banana'];
59+
60+
assertType("array{1: 'avocado', 2: 'apple', 3: 'banana'}", array_combine($a, $b));
61+
}
62+
63+
function withObjectKey() : void
64+
{
65+
$a = [new Foo, 'red', 'yellow'];
66+
$b = ['avocado', 'apple', 'banana'];
67+
68+
assertType("array{foo: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));
69+
assertType("non-empty-array<string, 'apple'|'avocado'|'banana'>|false", array_combine([new Bar, 'red', 'yellow'], $b));
70+
assertType("*NEVER*", array_combine([new Baz, 'red', 'yellow'], $b));
71+
}
72+
73+
/**
74+
* @param non-empty-array<int, 'foo'|'bar'|'baz'> $a
75+
* @param non-empty-array<int, 'apple'|'avocado'|'banana'> $b
76+
*/
77+
function withNonEmptyArray(array $a, array $b): void
78+
{
79+
assertType("non-empty-array<'bar'|'baz'|'foo', 'apple'|'avocado'|'banana'>|false", array_combine($a, $b));
80+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace ArrayCombinePHP8;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
/** @phpstan-return 'foo' */
10+
public function __toString(): string
11+
{
12+
return 'foo';
13+
}
14+
}
15+
16+
class Bar
17+
{
18+
public function __toString(): string
19+
{
20+
return 'bar';
21+
}
22+
}
23+
24+
class Baz {}
25+
26+
function withBoolKey(): void
27+
{
28+
$a = [true, 'red', 'yellow'];
29+
$b = ['avocado', 'apple', 'banana'];
30+
31+
assertType("array{1: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));
32+
33+
$c = [false, 'red', 'yellow'];
34+
$d = ['avocado', 'apple', 'banana'];
35+
36+
assertType("array{: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($c, $d));
37+
}
38+
39+
function withFloatKey(): void
40+
{
41+
$a = [1.5, 'red', 'yellow'];
42+
$b = ['avocado', 'apple', 'banana'];
43+
44+
assertType("array{1.5: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));
45+
}
46+
47+
function withIntegerKey(): void
48+
{
49+
$a = [1, 2, 3];
50+
$b = ['avocado', 'apple', 'banana'];
51+
52+
assertType("array{1: 'avocado', 2: 'apple', 3: 'banana'}", array_combine($a, $b));
53+
}
54+
55+
function withNumericStringKey(): void
56+
{
57+
$a = ["1", "2", "3"];
58+
$b = ['avocado', 'apple', 'banana'];
59+
60+
assertType("array{1: 'avocado', 2: 'apple', 3: 'banana'}", array_combine($a, $b));
61+
}
62+
63+
function withObjectKey() : void
64+
{
65+
$a = [new Foo, 'red', 'yellow'];
66+
$b = ['avocado', 'apple', 'banana'];
67+
68+
assertType("array{foo: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));
69+
assertType("non-empty-array<string, 'apple'|'avocado'|'banana'>", array_combine([new Bar, 'red', 'yellow'], $b));
70+
assertType("*NEVER*", array_combine([new Baz, 'red', 'yellow'], $b));
71+
}
72+
73+
/**
74+
* @param non-empty-array<int, 'foo'|'bar'|'baz'> $a
75+
* @param non-empty-array<int, 'apple'|'avocado'|'banana'> $b
76+
*/
77+
function withNonEmptyArray(array $a, array $b): void
78+
{
79+
assertType("non-empty-array<'bar'|'baz'|'foo', 'apple'|'avocado'|'banana'>", array_combine($a, $b));
80+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace ArrayFillKeys;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
/** @phpstan-return 'foo' */
10+
public function __toString(): string
11+
{
12+
return 'foo';
13+
}
14+
}
15+
16+
class Bar
17+
{
18+
public function __toString(): string
19+
{
20+
return 'bar';
21+
}
22+
}
23+
24+
class Baz {}
25+
26+
function withBoolKey() : array
27+
{
28+
assertType("array{1: 'b'}", array_fill_keys([true], 'b'));
29+
assertType("array{: 'b'}", array_fill_keys([false], 'b'));
30+
}
31+
32+
function withFloatKey() : array
33+
{
34+
assertType("array{1.5: 'b'}", array_fill_keys([1.5], 'b'));
35+
}
36+
37+
function withIntegerKey() : array
38+
{
39+
assertType("array{99: 'b'}", array_fill_keys([99], 'b'));
40+
}
41+
42+
function withNumericStringKey() : array
43+
{
44+
assertType("array{999: 'b'}", array_fill_keys(["999"], 'b'));
45+
}
46+
47+
function withObjectKey() : array
48+
{
49+
assertType("array{foo: 'b'}", array_fill_keys([new Foo()], 'b'));
50+
assertType("non-empty-array<string, 'b'>", array_fill_keys([new Bar()], 'b'));
51+
assertType("*NEVER*", array_fill_keys([new Baz()], 'b'));
52+
}

0 commit comments

Comments
 (0)