Skip to content

Commit 2132cc0

Browse files
herndlmondrejmirtes
authored andcommitted
Support tagged unions in array_merge
1 parent fd7bad3 commit 2132cc0

File tree

8 files changed

+103
-34
lines changed

8 files changed

+103
-34
lines changed

Diff for: phpstan-baseline.neon

-5
Original file line numberDiff line numberDiff line change
@@ -1312,11 +1312,6 @@ parameters:
13121312
count: 1
13131313
path: src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php
13141314

1315-
-
1316-
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#"
1317-
count: 4
1318-
path: src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php
1319-
13201315
-
13211316
message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#"
13221317
count: 16

Diff for: src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php

+25-29
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
use PhpParser\Node\Expr\FuncCall;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\FunctionReflection;
8-
use PHPStan\ShouldNotHappenException;
8+
use PHPStan\TrinaryLogic;
99
use PHPStan\Type\Accessory\AccessoryArrayListType;
1010
use PHPStan\Type\Accessory\NonEmptyArrayType;
1111
use PHPStan\Type\ArrayType;
1212
use PHPStan\Type\Constant\ConstantArrayType;
1313
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1414
use PHPStan\Type\Constant\ConstantIntegerType;
15+
use PHPStan\Type\Constant\ConstantStringType;
1516
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1617
use PHPStan\Type\IntegerType;
1718
use PHPStan\Type\NeverType;
@@ -39,58 +40,53 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3940

4041
$argTypes = [];
4142
$optionalArgTypes = [];
42-
$allConstant = true;
4343
foreach ($args as $arg) {
4444
$argType = $scope->getType($arg->value);
4545

4646
if ($arg->unpack) {
47-
if ($argType instanceof ConstantArrayType) {
48-
$argTypesFound = $argType->getValueTypes();
49-
} else {
50-
$argTypesFound = [$argType->getIterableValueType()];
51-
}
52-
53-
foreach ($argTypesFound as $argTypeFound) {
54-
$argTypes[] = $argTypeFound;
55-
if ($argTypeFound instanceof ConstantArrayType) {
56-
continue;
47+
if ($argType->isConstantArray()->yes()) {
48+
foreach ($argType->getConstantArrays() as $constantArray) {
49+
foreach ($constantArray->getValueTypes() as $valueType) {
50+
$argTypes[] = $valueType;
51+
}
5752
}
58-
$allConstant = false;
53+
} else {
54+
$argTypes[] = $argType->getIterableValueType();
5955
}
6056

6157
if (!$argType->isIterableAtLeastOnce()->yes()) {
6258
// unpacked params can be empty, making them optional
6359
$optionalArgTypesOffset = count($argTypes) - 1;
64-
foreach (array_keys($argTypesFound) as $key) {
60+
foreach (array_keys($argTypes) as $key) {
6561
$optionalArgTypes[] = $optionalArgTypesOffset + $key;
6662
}
6763
}
6864
} else {
6965
$argTypes[] = $argType;
70-
if (!$argType instanceof ConstantArrayType) {
71-
$allConstant = false;
72-
}
7366
}
7467
}
7568

76-
if ($allConstant) {
69+
$allConstant = TrinaryLogic::createYes()->lazyAnd(
70+
$argTypes,
71+
static fn (Type $argType) => $argType->isConstantArray(),
72+
);
73+
74+
if ($allConstant->yes()) {
7775
$newArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
7876
foreach ($argTypes as $argType) {
79-
if (!$argType instanceof ConstantArrayType) {
80-
throw new ShouldNotHappenException();
77+
/** @var array<int|string, ConstantIntegerType|ConstantStringType> $keyTypes */
78+
$keyTypes = [];
79+
foreach ($argType->getConstantArrays() as $constantArray) {
80+
foreach ($constantArray->getKeyTypes() as $keyType) {
81+
$keyTypes[$keyType->getValue()] = $keyType;
82+
}
8183
}
8284

83-
$keyTypes = $argType->getKeyTypes();
84-
$valueTypes = $argType->getValueTypes();
85-
$optionalKeys = $argType->getOptionalKeys();
86-
87-
foreach ($keyTypes as $k => $keyType) {
88-
$isOptional = in_array($k, $optionalKeys, true);
89-
85+
foreach ($keyTypes as $keyType) {
9086
$newArrayBuilder->setOffsetValueType(
9187
$keyType instanceof ConstantIntegerType ? null : $keyType,
92-
$valueTypes[$k],
93-
$isOptional,
88+
$argType->getOffsetValueType($keyType),
89+
!$argType->hasOffsetValueType($keyType)->yes(),
9490
);
9591
}
9692
}

Diff for: tests/PHPStan/Analyser/nsrt/array-merge2.php

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ 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', lall?: '3', 0: '2'|'4', 1: '3'|'6', lall2?: '3'}", array_merge(rand(0, 1) ? $array1 : $array2, []));
25+
assertType("array{foo?: 3, bar?: 3}", array_merge([], ...[rand(0, 1) ? ['foo' => 3] : ['bar' => 3]]));
26+
assertType("array{foo: '1', bar: '2'|'4', lall?: '3', 0: '2'|'4', 1: '3'|'6', lall2?: '3'}", array_merge([], ...[rand(0, 1) ? $array1 : $array2]));
27+
assertType("array{foo: 1, bar: 2, 0: 2, 1: 3}", array_merge(['foo' => 4, 'bar' => 5], ...[['foo' => 1, 'bar' => 2], [2, 3]]));
2428
}
2529

2630
/**

Diff for: tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

+9
Original file line numberDiff line numberDiff line change
@@ -1598,6 +1598,15 @@ public function testBug9399(): void
15981598
$this->analyse([__DIR__ . '/data/bug-9399.php'], []);
15991599
}
16001600

1601+
public function testBug9559(): void
1602+
{
1603+
if (PHP_VERSION_ID < 80000) {
1604+
$this->markTestSkipped('Test requires PHP 8.0');
1605+
}
1606+
1607+
$this->analyse([__DIR__ . '/data/bug-9559.php'], []);
1608+
}
1609+
16011610
public function testBug9923(): void
16021611
{
16031612
if (PHP_VERSION_ID < 80000) {

Diff for: tests/PHPStan/Rules/Functions/data/bug-9559.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9559;
4+
5+
class ZZ {};
6+
function foo(?ZZ $z = null, ?int $a = 0, ?string $b = "x"): string { return "bah"; }
7+
8+
function doit(int $x): void {
9+
$call = [];
10+
if ($x) $call['a'] = 45;
11+
foo(...array_merge($call, [ "b" => "3" ]));
12+
}

Diff for: tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php

+10
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,16 @@ public function testBug8573(): void
846846
$this->analyse([__DIR__ . '/data/bug-8573.php'], []);
847847
}
848848

849+
public function testBug8632(): void
850+
{
851+
$this->analyse([__DIR__ . '/data/bug-8632.php'], []);
852+
}
853+
854+
public function testBug7857(): void
855+
{
856+
$this->analyse([__DIR__ . '/data/bug-7857.php'], []);
857+
}
858+
849859
public function testBug8879(): void
850860
{
851861
$this->analyse([__DIR__ . '/data/bug-8879.php'], []);

Diff for: tests/PHPStan/Rules/Methods/data/bug-7857.php

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug7857;
4+
5+
class Paginator
6+
{
7+
/**
8+
* @return array{page: int, perPage?: int}
9+
*/
10+
public function toArray(int $page, ?int $perPage): array
11+
{
12+
return array_merge(
13+
['page' => $page],
14+
$perPage !== null ? ['perPage' => $perPage] : []
15+
);
16+
}
17+
}

Diff for: tests/PHPStan/Rules/Methods/data/bug-8632.php

+26
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)