Skip to content

Commit 565248d

Browse files
authored
array_merge lost non-empty-string keys type
1 parent ae67ac8 commit 565248d

File tree

4 files changed

+121
-11
lines changed

4 files changed

+121
-11
lines changed

src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\FunctionReflection;
88
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\ShouldNotHappenException;
910
use PHPStan\Type\Accessory\NonEmptyArrayType;
1011
use PHPStan\Type\ArrayType;
1112
use PHPStan\Type\Constant\ConstantArrayType;
13+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1214
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
13-
use PHPStan\Type\GeneralizePrecision;
1415
use PHPStan\Type\NeverType;
1516
use PHPStan\Type\Type;
1617
use PHPStan\Type\TypeCombinator;
1718
use PHPStan\Type\UnionType;
19+
use function in_array;
1820

1921
class ArrayMergeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2022
{
@@ -26,15 +28,58 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo
2628

2729
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
2830
{
29-
if (!isset($functionCall->getArgs()[0])) {
31+
$args = $functionCall->getArgs();
32+
33+
if (!isset($args[0])) {
3034
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
3135
}
3236

37+
$argTypes = [];
38+
$allConstant = true;
39+
foreach ($args as $i => $arg) {
40+
$argType = $scope->getType($arg->value);
41+
$argTypes[$i] = $argType;
42+
43+
if (!$arg->unpack && $argType instanceof ConstantArrayType) {
44+
continue;
45+
}
46+
47+
$allConstant = false;
48+
}
49+
50+
if ($allConstant) {
51+
$newArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
52+
foreach ($args as $i => $arg) {
53+
$argType = $argTypes[$i];
54+
55+
if (!$argType instanceof ConstantArrayType) {
56+
throw new ShouldNotHappenException();
57+
}
58+
59+
$keyTypes = $argType->getKeyTypes();
60+
$valueTypes = $argType->getValueTypes();
61+
$optionalKeys = $argType->getOptionalKeys();
62+
63+
foreach ($keyTypes as $k => $keyType) {
64+
$isOptional = in_array($k, $optionalKeys, true);
65+
66+
$newArrayBuilder->setOffsetValueType(
67+
$keyType,
68+
$valueTypes[$k],
69+
$isOptional,
70+
);
71+
}
72+
}
73+
74+
return $newArrayBuilder->getArray();
75+
}
76+
3377
$keyTypes = [];
3478
$valueTypes = [];
3579
$nonEmpty = false;
36-
foreach ($functionCall->getArgs() as $arg) {
37-
$argType = $scope->getType($arg->value);
80+
foreach ($args as $i => $arg) {
81+
$argType = $argTypes[$i];
82+
3883
if ($arg->unpack) {
3984
$argType = $argType->getIterableValueType();
4085
if ($argType instanceof UnionType) {
@@ -44,7 +89,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
4489
}
4590
}
4691

47-
$keyTypes[] = $argType->getIterableKeyType()->generalize(GeneralizePrecision::moreSpecific());
92+
$keyTypes[] = $argType->getIterableKeyType();
4893
$valueTypes[] = $argType->getIterableValueType();
4994

5095
if (!$argType->isIterableAtLeastOnce()->yes()) {

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4707,31 +4707,31 @@ public function dataArrayFunctions(): array
47074707
'array_values($generalStringKeys)',
47084708
],
47094709
[
4710-
'non-empty-array<int|(literal-string&non-empty-string), stdClass>',
4710+
'array{foo: stdClass, 1: stdClass}',
47114711
'array_merge($stringOrIntegerKeys)',
47124712
],
47134713
[
47144714
'array<int|string, DateTimeImmutable|int>',
47154715
'array_merge($generalStringKeys, $generalDateTimeValues)',
47164716
],
47174717
[
4718-
'non-empty-array<int|string, int|stdClass>',
4718+
'non-empty-array<1|string, int|stdClass>',
47194719
'array_merge($generalStringKeys, $stringOrIntegerKeys)',
47204720
],
47214721
[
4722-
'non-empty-array<int|string, int|stdClass>',
4722+
'non-empty-array<1|string, int|stdClass>',
47234723
'array_merge($stringOrIntegerKeys, $generalStringKeys)',
47244724
],
47254725
[
4726-
'non-empty-array<int|(literal-string&non-empty-string), \'foo\'|stdClass>',
4726+
'array{foo: stdClass, bar: stdClass, 1: stdClass}',
47274727
'array_merge($stringKeys, $stringOrIntegerKeys)',
47284728
],
47294729
[
4730-
'non-empty-array<int|(literal-string&non-empty-string), \'foo\'|stdClass>',
4730+
"array{foo: 'foo', 1: stdClass, bar: stdClass}",
47314731
'array_merge($stringOrIntegerKeys, $stringKeys)',
47324732
],
47334733
[
4734-
'non-empty-array<int|(literal-string&non-empty-string), 2|4|\'a\'|\'b\'|\'green\'|\'red\'|\'trapezoid\'>',
4734+
"array{color: 'green', 0: 'a', 1: 'b', shape: 'trapezoid', 2: 4}",
47354735
'array_merge(array("color" => "red", 2, 4), array("a", "b", "color" => "green", "shape" => "trapezoid", 4))',
47364736
],
47374737
[

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,7 @@ public function dataFileAsserts(): iterable
846846

847847
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6917.php');
848848
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6936-limit.php');
849+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6927.php');
849850
}
850851

851852
/**
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace Bug6927;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
/**
10+
* @param array<non-empty-string, string> $params1
11+
* @param array<non-empty-string, string> $params2
12+
*/
13+
function foo1(array $params1, array $params2): void
14+
{
15+
$params2 = array_merge($params1, $params2);
16+
17+
assertType('array<non-empty-string, string>', $params2);
18+
}
19+
20+
/**
21+
* @param array<non-empty-string, string> $params1
22+
* @param array<string, string> $params2
23+
*/
24+
function foo2(array $params1, array $params2): void
25+
{
26+
$params2 = array_merge($params1, $params2);
27+
28+
assertType('array<string, string>', $params2);
29+
}
30+
31+
/**
32+
* @param array<string, string> $params1
33+
* @param array<non-empty-string, string> $params2
34+
*/
35+
function foo3(array $params1, array $params2): void
36+
{
37+
$params2 = array_merge($params1, $params2);
38+
39+
assertType('array<string, string>', $params2);
40+
}
41+
42+
/**
43+
* @param array<literal-string&non-empty-string, string> $params1
44+
* @param array<non-empty-string, string> $params2
45+
*/
46+
function foo4(array $params1, array $params2): void
47+
{
48+
$params2 = array_merge($params1, $params2);
49+
50+
assertType('array<non-empty-string, string>', $params2);
51+
}
52+
53+
/**
54+
* @param array{return: int, stdout: string, stderr: string} $params1
55+
* @param array{return: int, stdout?: string, stderr?: string} $params2
56+
*/
57+
function foo5(array $params1, array $params2): void
58+
{
59+
$params3 = array_merge($params1, $params2);
60+
61+
assertType('array{return: int, stdout: string, stderr: string}', $params3);
62+
}
63+
64+
}

0 commit comments

Comments
 (0)