Skip to content

Commit d4bdaf7

Browse files
committed
Support tagged unions in array_merge
1 parent cdf5110 commit d4bdaf7

File tree

5 files changed

+69
-14
lines changed

5 files changed

+69
-14
lines changed

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1470,7 +1470,7 @@ parameters:
14701470
-
14711471
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#'
14721472
identifier: phpstanApi.instanceofType
1473-
count: 4
1473+
count: 2
14741474
path: src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php
14751475

14761476
-

src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\FunctionReflection;
88
use PHPStan\ShouldNotHappenException;
9+
use PHPStan\TrinaryLogic;
910
use PHPStan\Type\Accessory\AccessoryArrayListType;
1011
use PHPStan\Type\Accessory\NonEmptyArrayType;
1112
use PHPStan\Type\ArrayType;
@@ -39,7 +40,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3940

4041
$argTypes = [];
4142
$optionalArgTypes = [];
42-
$allConstant = true;
43+
$allConstant = TrinaryLogic::createYes();
4344
foreach ($args as $arg) {
4445
$argType = $scope->getType($arg->value);
4546

@@ -52,10 +53,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
5253

5354
foreach ($argTypesFound as $argTypeFound) {
5455
$argTypes[] = $argTypeFound;
55-
if ($argTypeFound instanceof ConstantArrayType) {
56-
continue;
57-
}
58-
$allConstant = false;
56+
$allConstant = $allConstant->and($argTypeFound->isConstantArray());
5957
}
6058

6159
if (!$argType->isIterableAtLeastOnce()->yes()) {
@@ -67,22 +65,21 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
6765
}
6866
} else {
6967
$argTypes[] = $argType;
70-
if (!$argType instanceof ConstantArrayType) {
71-
$allConstant = false;
72-
}
68+
$allConstant = $allConstant->and($argType->isConstantArray());
7369
}
7470
}
7571

76-
if ($allConstant) {
72+
if ($allConstant->yes()) {
7773
$newArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
7874
foreach ($argTypes as $argType) {
79-
if (!$argType instanceof ConstantArrayType) {
75+
$constantArrayType = $this->untagConstantArrayUnion($argType);
76+
if (!$constantArrayType instanceof ConstantArrayType) {
8077
throw new ShouldNotHappenException();
8178
}
8279

83-
$keyTypes = $argType->getKeyTypes();
84-
$valueTypes = $argType->getValueTypes();
85-
$optionalKeys = $argType->getOptionalKeys();
80+
$keyTypes = $constantArrayType->getKeyTypes();
81+
$valueTypes = $constantArrayType->getValueTypes();
82+
$optionalKeys = $constantArrayType->getOptionalKeys();
8683

8784
foreach ($keyTypes as $k => $keyType) {
8885
$isOptional = in_array($k, $optionalKeys, true);
@@ -138,4 +135,29 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
138135
return $arrayType;
139136
}
140137

138+
/**
139+
* array{0: 17, foo: 'bar'}|array{0: 19, foo: 'bar', fofoo: 'barbar'}
140+
* ->
141+
* array{0: 17|19, foo: 'bar', foofo?: 'barbar'}
142+
*/
143+
private function untagConstantArrayUnion(Type $constantArrayType): Type
144+
{
145+
$constantArrayTypes = $constantArrayType->getConstantArrays();
146+
if (count($constantArrayTypes) === 1) {
147+
return $constantArrayTypes[0];
148+
}
149+
150+
$builder = ConstantArrayTypeBuilder::createEmpty();
151+
$keyTypes = $constantArrayType->getKeysArray()->getIterableValueType()->getFiniteTypes();
152+
foreach ($keyTypes as $keyType) {
153+
$builder->setOffsetValueType(
154+
$keyType,
155+
$constantArrayType->getOffsetValueType($keyType),
156+
!$constantArrayType->hasOffsetValueType($keyType)->yes(),
157+
);
158+
}
159+
160+
return $builder->getArray();
161+
}
162+
141163
}

tests/PHPStan/Analyser/nsrt/array-merge2.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public function arrayMergeArrayShapes($array1, $array2): void
2121
assertType("array{foo: '1', bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1));
2222
assertType("array{foo: 3, bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1, ['foo' => 3]));
2323
assertType("array{foo: 3, bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1, ...[['foo' => 3]]));
24+
assertType("array{foo: '1', bar: '2'|'4', lall2?: '3', lall?: '3', 0: '2'|'4', 1: '3'|'6'}", array_merge(rand(0, 1) ? $array1 : $array2, []));
25+
assertType("array{foo?: 3, bar?: 3}", array_merge([], ...[rand(0, 1) ? ['foo' => 3] : ['bar' => 3]]));
2426
}
2527

2628
/**

tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,11 @@ public function testBug8573(): void
877877
$this->analyse([__DIR__ . '/data/bug-8573.php'], []);
878878
}
879879

880+
public function testBug8632(): void
881+
{
882+
$this->analyse([__DIR__ . '/data/bug-8632.php'], []);
883+
}
884+
880885
public function testBug8879(): void
881886
{
882887
$this->analyse([__DIR__ . '/data/bug-8879.php'], []);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug8632;
4+
5+
class HelloWorld
6+
{
7+
/**
8+
* @return array{
9+
* id?: int,
10+
* categories?: string[],
11+
* }
12+
*/
13+
public function test(bool $foo): array
14+
{
15+
if ($foo) {
16+
$arr = [
17+
'id' => 1,
18+
'categories' => ['news'],
19+
];
20+
} else {
21+
$arr = [];
22+
}
23+
24+
return array_merge($arr, []);
25+
}
26+
}

0 commit comments

Comments
 (0)