Skip to content

Commit 7114465

Browse files
vokuondrejmirtes
authored andcommitted
Add DynamicFunctionReturnTypeExtension for array_replace
1 parent b4b44cf commit 7114465

File tree

6 files changed

+167
-0
lines changed

6 files changed

+167
-0
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,11 @@ services:
973973
tags:
974974
- phpstan.broker.dynamicFunctionReturnTypeExtension
975975

976+
-
977+
class: PHPStan\Type\Php\ArrayReplaceFunctionDynamicReturnTypeExtension
978+
tags:
979+
- phpstan.broker.dynamicFunctionReturnTypeExtension
980+
976981
-
977982
class: PHPStan\Type\Php\ArrayKeysFunctionDynamicReturnTypeExtension
978983
tags:
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Type\Accessory\NonEmptyArrayType;
10+
use PHPStan\Type\ArrayType;
11+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
12+
use PHPStan\Type\GeneralizePrecision;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\TypeCombinator;
15+
use PHPStan\Type\TypeUtils;
16+
17+
class ArrayReplaceFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
18+
{
19+
20+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
21+
{
22+
return $functionReflection->getName() === 'array_replace';
23+
}
24+
25+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
26+
{
27+
if (!isset($functionCall->args[0])) {
28+
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
29+
}
30+
31+
$keyTypes = [];
32+
$valueTypes = [];
33+
$returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
34+
$returnedArrayBuilderFilled = false;
35+
$nonEmpty = false;
36+
foreach ($functionCall->args as $arg) {
37+
$argType = $scope->getType($arg->value);
38+
39+
if ($arg->unpack) {
40+
$argType = $argType->getIterableValueType();
41+
}
42+
43+
$arrays = TypeUtils::getConstantArrays($argType);
44+
if (count($arrays) > 0) {
45+
foreach ($arrays as $constantArray) {
46+
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
47+
$returnedArrayBuilderFilled = true;
48+
49+
$returnedArrayBuilder->setOffsetValueType(
50+
$keyType,
51+
$constantArray->getValueTypes()[$i]
52+
);
53+
}
54+
}
55+
56+
} else {
57+
$keyTypes[] = TypeUtils::generalizeType($argType->getIterableKeyType(), GeneralizePrecision::moreSpecific());
58+
$valueTypes[] = $argType->getIterableValueType();
59+
}
60+
61+
if (!$argType->isIterableAtLeastOnce()->yes()) {
62+
continue;
63+
}
64+
65+
$nonEmpty = true;
66+
}
67+
68+
if (count($keyTypes) > 0) {
69+
$arrayType = new ArrayType(
70+
TypeCombinator::union(...$keyTypes),
71+
TypeCombinator::union(...$valueTypes)
72+
);
73+
74+
if ($returnedArrayBuilderFilled) {
75+
$arrayType = TypeCombinator::union($returnedArrayBuilder->getArray(), $arrayType);
76+
}
77+
} elseif ($returnedArrayBuilderFilled) {
78+
$arrayType = $returnedArrayBuilder->getArray();
79+
} else {
80+
$arrayType = new ArrayType(
81+
TypeCombinator::union(...$keyTypes),
82+
TypeCombinator::union(...$valueTypes)
83+
);
84+
}
85+
86+
if ($nonEmpty) {
87+
$arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
88+
}
89+
90+
return $arrayType;
91+
}
92+
93+
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5152,6 +5152,10 @@ public function dataArrayFunctions(): array
51525152
'array<int, int>',
51535153
'array_values($generalStringKeys)',
51545154
],
5155+
[
5156+
"array('foo' => 'foo', 1 => stdClass, 'bar' => stdClass)",
5157+
'array_replace($stringOrIntegerKeys, $stringKeys)',
5158+
],
51555159
[
51565160
"array('foo' => stdClass, 0 => stdClass)",
51575161
'array_merge($stringOrIntegerKeys)',

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ public function dataFileAsserts(): iterable
136136

137137
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-merge.php');
138138

139+
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-replace.php');
140+
139141
yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array.php');
140142

141143
if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace NonEmptyArray;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param array{foo: '1', bar: '2', lall: '3', 2: '2', 3: '3'} $array1
12+
* @param array{foo: '1', bar: '4', lall2: '3', 2: '4', 3: '6'} $array2
13+
*/
14+
public function arrayReplaceArrayShapes($array1, $array2): void
15+
{
16+
assertType("array('foo' => '1', 'bar' => '2', 'lall' => '3', 2 => '2', 3 => '3')", array_replace($array1));
17+
assertType("array('foo' => '1', 'bar' => '2', 'lall' => '3', 2 => '2', 3 => '3')", array_replace([], $array1));
18+
assertType("array('foo' => '1', 'bar' => '2', 'lall' => '3', 2 => '2', 3 => '3')", array_replace($array1, []));
19+
assertType("array('foo' => '1', 'bar' => '2', 'lall' => '3', 2 => '2', 3 => '3')", array_replace($array1, $array1));
20+
assertType("array('foo' => '1', 'bar' => '4', 'lall' => '3', 2 => '4', 3 => '6', 'lall2' => '3')", array_replace($array1, $array2));
21+
assertType("array('foo' => '1', 'bar' => '2', 'lall2' => '3', 2 => '2', 3 => '3', 'lall' => '3')", array_replace($array2, $array1));
22+
assertType("array('foo' => 3, 'bar' => '2', 'lall2' => '3', 2 => '2', 3 => '3', 'lall' => '3')", array_replace($array2, $array1, ['foo' => 3]));
23+
assertType("array('foo' => 3, 'bar' => '2', 'lall2' => '3', 2 => '2', 3 => '3', 'lall' => '3')", array_replace($array2, $array1, ...[['foo' => 3]]));
24+
}
25+
26+
/**
27+
* @param int[] $array1
28+
* @param string[] $array2
29+
*/
30+
public function arrayReplaceSimple($array1, $array2): void
31+
{
32+
assertType("array<int>", array_merge($array1, $array1));
33+
assertType("array<int|string>", array_merge($array1, $array2));
34+
assertType("array<int|string>", array_merge($array2, $array1));
35+
}
36+
37+
/**
38+
* @param array<int, int|string> $array1
39+
* @param array<int, bool|float> $array2
40+
*/
41+
public function arrayReplaceUnionType($array1, $array2): void
42+
{
43+
assertType("array<int, int|string>", array_merge($array1, $array1));
44+
assertType("array<int, bool|float|int|string>", array_merge($array1, $array2));
45+
assertType("array<int, bool|float|int|string>", array_merge($array2, $array1));
46+
}
47+
48+
/**
49+
* @param array<int, array{bar: '2'}|array{foo: '1'}> $array1
50+
* @param array<int, array{bar: '3'}|array{foo: '2'}> $array2
51+
*/
52+
public function arrayReplaceUnionTypeArrayShapes($array1, $array2): void
53+
{
54+
assertType("array<int, array('bar' => '2')|array('foo' => '1')>", array_merge($array1, $array1));
55+
assertType("array<int, array('bar' => '2'|'3')|array('foo' => '1'|'2')>", array_merge($array1, $array2));
56+
assertType("array<int, array('bar' => '2'|'3')|array('foo' => '1'|'2')>", array_merge($array2, $array1));
57+
}
58+
}

tests/PHPStan/Analyser/data/non-empty-array.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public function arrayFunctions($array, $list, $stringArray): void
4949
assertType('array&nonEmpty', array_merge($array, []));
5050
assertType('array&nonEmpty', array_merge($array, $array));
5151

52+
assertType('array&nonEmpty', array_replace($array));
53+
assertType('array&nonEmpty', array_replace([], $array));
54+
assertType('array&nonEmpty', array_replace($array, []));
55+
assertType('array&nonEmpty', array_replace($array, $array));
56+
5257
assertType('array<int|string, (int|string)>&nonEmpty', array_flip($array));
5358
assertType('array<string, (int|string)>&nonEmpty', array_flip($stringArray));
5459
}

0 commit comments

Comments
 (0)