Skip to content

Commit

Permalink
Support tagged unions in array_merge
Browse files Browse the repository at this point in the history
  • Loading branch information
herndlm committed Jan 4, 2025
1 parent cdf5110 commit d4bdaf7
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 14 deletions.
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1470,7 +1470,7 @@ parameters:
-
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#'
identifier: phpstanApi.instanceofType
count: 4
count: 2
path: src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php

-
Expand Down
48 changes: 35 additions & 13 deletions src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryArrayListType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
Expand Down Expand Up @@ -39,7 +40,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,

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

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

foreach ($argTypesFound as $argTypeFound) {
$argTypes[] = $argTypeFound;
if ($argTypeFound instanceof ConstantArrayType) {
continue;
}
$allConstant = false;
$allConstant = $allConstant->and($argTypeFound->isConstantArray());
}

if (!$argType->isIterableAtLeastOnce()->yes()) {
Expand All @@ -67,22 +65,21 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
}
} else {
$argTypes[] = $argType;
if (!$argType instanceof ConstantArrayType) {
$allConstant = false;
}
$allConstant = $allConstant->and($argType->isConstantArray());
}
}

if ($allConstant) {
if ($allConstant->yes()) {
$newArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
foreach ($argTypes as $argType) {
if (!$argType instanceof ConstantArrayType) {
$constantArrayType = $this->untagConstantArrayUnion($argType);
if (!$constantArrayType instanceof ConstantArrayType) {
throw new ShouldNotHappenException();
}

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

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

/**
* array{0: 17, foo: 'bar'}|array{0: 19, foo: 'bar', fofoo: 'barbar'}
* ->
* array{0: 17|19, foo: 'bar', foofo?: 'barbar'}
*/
private function untagConstantArrayUnion(Type $constantArrayType): Type
{
$constantArrayTypes = $constantArrayType->getConstantArrays();
if (count($constantArrayTypes) === 1) {
return $constantArrayTypes[0];
}

$builder = ConstantArrayTypeBuilder::createEmpty();
$keyTypes = $constantArrayType->getKeysArray()->getIterableValueType()->getFiniteTypes();
foreach ($keyTypes as $keyType) {
$builder->setOffsetValueType(
$keyType,
$constantArrayType->getOffsetValueType($keyType),
!$constantArrayType->hasOffsetValueType($keyType)->yes(),
);
}

return $builder->getArray();
}

}
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/nsrt/array-merge2.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public function arrayMergeArrayShapes($array1, $array2): void
assertType("array{foo: '1', bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1));
assertType("array{foo: 3, bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1, ['foo' => 3]));
assertType("array{foo: 3, bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1, ...[['foo' => 3]]));
assertType("array{foo: '1', bar: '2'|'4', lall2?: '3', lall?: '3', 0: '2'|'4', 1: '3'|'6'}", array_merge(rand(0, 1) ? $array1 : $array2, []));
assertType("array{foo?: 3, bar?: 3}", array_merge([], ...[rand(0, 1) ? ['foo' => 3] : ['bar' => 3]]));
}

/**
Expand Down
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,11 @@ public function testBug8573(): void
$this->analyse([__DIR__ . '/data/bug-8573.php'], []);
}

public function testBug8632(): void
{
$this->analyse([__DIR__ . '/data/bug-8632.php'], []);
}

public function testBug8879(): void
{
$this->analyse([__DIR__ . '/data/bug-8879.php'], []);
Expand Down
26 changes: 26 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-8632.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php declare(strict_types = 1);

namespace Bug8632;

class HelloWorld
{
/**
* @return array{
* id?: int,
* categories?: string[],
* }
*/
public function test(bool $foo): array
{
if ($foo) {
$arr = [
'id' => 1,
'categories' => ['news'],
];
} else {
$arr = [];
}

return array_merge($arr, []);
}
}

0 comments on commit d4bdaf7

Please sign in to comment.