Skip to content

Commit

Permalink
Preserve TemplateTypeArray when merging array types
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Jan 16, 2024
1 parent 3d4486d commit 8342785
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 4 deletions.
33 changes: 29 additions & 4 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\TemplateArrayType;
use PHPStan\Type\Generic\TemplateBenevolentUnionType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeFactory;
Expand Down Expand Up @@ -699,11 +700,35 @@ private static function processArrayTypes(array $arrayTypes): array
}

if ($generalArrayOccurred) {
$scopes = [];
$useTemplateArray = true;
foreach ($arrayTypes as $arrayType) {
if (!$arrayType instanceof TemplateArrayType) {
$useTemplateArray = false;
break;
}

$scopes[$arrayType->getScope()->describe()] = $arrayType;
}

$arrayType = new ArrayType(
self::union(...$keyTypesForGeneralArray),
self::union(...self::optimizeConstantArrays($valueTypesForGeneralArray)),
);

if ($useTemplateArray && count($scopes) === 1) {
$templateArray = array_values($scopes)[0];
$arrayType = new TemplateArrayType(
$templateArray->getScope(),
$templateArray->getStrategy(),
$templateArray->getVariance(),
$templateArray->getName(),
$arrayType,
);
}

return [
self::intersect(new ArrayType(
self::union(...$keyTypesForGeneralArray),
self::union(...self::optimizeConstantArrays($valueTypesForGeneralArray)),
), ...$accessoryTypes),
self::intersect($arrayType, ...$accessoryTypes),
];
}

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 @@ -42,6 +42,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-enum-class-string.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7162.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10201.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10445.php');
}

require_once __DIR__ . '/data/generic-generalization.php';
Expand Down
49 changes: 49 additions & 0 deletions tests/PHPStan/Analyser/data/bug-10445.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php // lint >= 8.1

namespace Bug10445;

use function PHPStan\Testing\assertType;

enum Error {
case A;
case B;
case C;
}

/**
* @template T of array
*/
class Response
{
/**
* @param ?T $data
* @param Error[] $errors
*/
public function __construct(
public ?array $data,
public array $errors = [],
) {
}

/**
* @return array{
* result: ?T,
* errors?: string[],
* }
*/
public function format(): array
{
$output = [
'result' => $this->data,
];
assertType('array{result: T of array (class Bug10445\Response, argument)|null}', $output);
if (count($this->errors) > 0) {
$output['errors'] = array_map(fn ($e) => $e->name, $this->errors);
assertType("array{result: T of array (class Bug10445\Response, argument)|null, errors: non-empty-array<'A'|'B'|'C'>}", $output);
} else {
assertType('array{result: T of array (class Bug10445\Response, argument)|null}', $output);
}
assertType("array{result: T of array (class Bug10445\Response, argument)|null, errors?: non-empty-array<'A'|'B'|'C'>}", $output);
return $output;
}
}

0 comments on commit 8342785

Please sign in to comment.