Skip to content

Commit

Permalink
array_merge lost non-empty-string keys type
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Mar 30, 2022
1 parent ae67ac8 commit 565248d
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 11 deletions.
55 changes: 50 additions & 5 deletions src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use function in_array;

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

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
if (!isset($functionCall->getArgs()[0])) {
$args = $functionCall->getArgs();

if (!isset($args[0])) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

$argTypes = [];
$allConstant = true;
foreach ($args as $i => $arg) {
$argType = $scope->getType($arg->value);
$argTypes[$i] = $argType;

if (!$arg->unpack && $argType instanceof ConstantArrayType) {
continue;
}

$allConstant = false;
}

if ($allConstant) {
$newArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
foreach ($args as $i => $arg) {
$argType = $argTypes[$i];

if (!$argType instanceof ConstantArrayType) {
throw new ShouldNotHappenException();
}

$keyTypes = $argType->getKeyTypes();
$valueTypes = $argType->getValueTypes();
$optionalKeys = $argType->getOptionalKeys();

foreach ($keyTypes as $k => $keyType) {
$isOptional = in_array($k, $optionalKeys, true);

$newArrayBuilder->setOffsetValueType(
$keyType,
$valueTypes[$k],
$isOptional,
);
}
}

return $newArrayBuilder->getArray();
}

$keyTypes = [];
$valueTypes = [];
$nonEmpty = false;
foreach ($functionCall->getArgs() as $arg) {
$argType = $scope->getType($arg->value);
foreach ($args as $i => $arg) {
$argType = $argTypes[$i];

if ($arg->unpack) {
$argType = $argType->getIterableValueType();
if ($argType instanceof UnionType) {
Expand All @@ -44,7 +89,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
}
}

$keyTypes[] = $argType->getIterableKeyType()->generalize(GeneralizePrecision::moreSpecific());
$keyTypes[] = $argType->getIterableKeyType();
$valueTypes[] = $argType->getIterableValueType();

if (!$argType->isIterableAtLeastOnce()->yes()) {
Expand Down
12 changes: 6 additions & 6 deletions tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4707,31 +4707,31 @@ public function dataArrayFunctions(): array
'array_values($generalStringKeys)',
],
[
'non-empty-array<int|(literal-string&non-empty-string), stdClass>',
'array{foo: stdClass, 1: stdClass}',
'array_merge($stringOrIntegerKeys)',
],
[
'array<int|string, DateTimeImmutable|int>',
'array_merge($generalStringKeys, $generalDateTimeValues)',
],
[
'non-empty-array<int|string, int|stdClass>',
'non-empty-array<1|string, int|stdClass>',
'array_merge($generalStringKeys, $stringOrIntegerKeys)',
],
[
'non-empty-array<int|string, int|stdClass>',
'non-empty-array<1|string, int|stdClass>',
'array_merge($stringOrIntegerKeys, $generalStringKeys)',
],
[
'non-empty-array<int|(literal-string&non-empty-string), \'foo\'|stdClass>',
'array{foo: stdClass, bar: stdClass, 1: stdClass}',
'array_merge($stringKeys, $stringOrIntegerKeys)',
],
[
'non-empty-array<int|(literal-string&non-empty-string), \'foo\'|stdClass>',
"array{foo: 'foo', 1: stdClass, bar: stdClass}",
'array_merge($stringOrIntegerKeys, $stringKeys)',
],
[
'non-empty-array<int|(literal-string&non-empty-string), 2|4|\'a\'|\'b\'|\'green\'|\'red\'|\'trapezoid\'>',
"array{color: 'green', 0: 'a', 1: 'b', shape: 'trapezoid', 2: 4}",
'array_merge(array("color" => "red", 2, 4), array("a", "b", "color" => "green", "shape" => "trapezoid", 4))',
],
[
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ public function dataFileAsserts(): iterable

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6917.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6936-limit.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6927.php');
}

/**
Expand Down
64 changes: 64 additions & 0 deletions tests/PHPStan/Analyser/data/bug-6927.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace Bug6927;

use function PHPStan\Testing\assertType;

class Foo
{
/**
* @param array<non-empty-string, string> $params1
* @param array<non-empty-string, string> $params2
*/
function foo1(array $params1, array $params2): void
{
$params2 = array_merge($params1, $params2);

assertType('array<non-empty-string, string>', $params2);
}

/**
* @param array<non-empty-string, string> $params1
* @param array<string, string> $params2
*/
function foo2(array $params1, array $params2): void
{
$params2 = array_merge($params1, $params2);

assertType('array<string, string>', $params2);
}

/**
* @param array<string, string> $params1
* @param array<non-empty-string, string> $params2
*/
function foo3(array $params1, array $params2): void
{
$params2 = array_merge($params1, $params2);

assertType('array<string, string>', $params2);
}

/**
* @param array<literal-string&non-empty-string, string> $params1
* @param array<non-empty-string, string> $params2
*/
function foo4(array $params1, array $params2): void
{
$params2 = array_merge($params1, $params2);

assertType('array<non-empty-string, string>', $params2);
}

/**
* @param array{return: int, stdout: string, stderr: string} $params1
* @param array{return: int, stdout?: string, stderr?: string} $params2
*/
function foo5(array $params1, array $params2): void
{
$params3 = array_merge($params1, $params2);

assertType('array{return: int, stdout: string, stderr: string}', $params3);
}

}

0 comments on commit 565248d

Please sign in to comment.