Skip to content

Commit 8342785

Browse files
committed
Preserve TemplateTypeArray when merging array types
1 parent 3d4486d commit 8342785

3 files changed

Lines changed: 79 additions & 4 deletions

File tree

src/Type/TypeCombinator.php

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use PHPStan\Type\Constant\ConstantIntegerType;
1818
use PHPStan\Type\Constant\ConstantStringType;
1919
use PHPStan\Type\Generic\GenericClassStringType;
20+
use PHPStan\Type\Generic\TemplateArrayType;
2021
use PHPStan\Type\Generic\TemplateBenevolentUnionType;
2122
use PHPStan\Type\Generic\TemplateType;
2223
use PHPStan\Type\Generic\TemplateTypeFactory;
@@ -699,11 +700,35 @@ private static function processArrayTypes(array $arrayTypes): array
699700
}
700701

701702
if ($generalArrayOccurred) {
703+
$scopes = [];
704+
$useTemplateArray = true;
705+
foreach ($arrayTypes as $arrayType) {
706+
if (!$arrayType instanceof TemplateArrayType) {
707+
$useTemplateArray = false;
708+
break;
709+
}
710+
711+
$scopes[$arrayType->getScope()->describe()] = $arrayType;
712+
}
713+
714+
$arrayType = new ArrayType(
715+
self::union(...$keyTypesForGeneralArray),
716+
self::union(...self::optimizeConstantArrays($valueTypesForGeneralArray)),
717+
);
718+
719+
if ($useTemplateArray && count($scopes) === 1) {
720+
$templateArray = array_values($scopes)[0];
721+
$arrayType = new TemplateArrayType(
722+
$templateArray->getScope(),
723+
$templateArray->getStrategy(),
724+
$templateArray->getVariance(),
725+
$templateArray->getName(),
726+
$arrayType,
727+
);
728+
}
729+
702730
return [
703-
self::intersect(new ArrayType(
704-
self::union(...$keyTypesForGeneralArray),
705-
self::union(...self::optimizeConstantArrays($valueTypesForGeneralArray)),
706-
), ...$accessoryTypes),
731+
self::intersect($arrayType, ...$accessoryTypes),
707732
];
708733
}
709734

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public function dataFileAsserts(): iterable
4242
yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-enum-class-string.php');
4343
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7162.php');
4444
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10201.php');
45+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10445.php');
4546
}
4647

4748
require_once __DIR__ . '/data/generic-generalization.php';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug10445;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
enum Error {
8+
case A;
9+
case B;
10+
case C;
11+
}
12+
13+
/**
14+
* @template T of array
15+
*/
16+
class Response
17+
{
18+
/**
19+
* @param ?T $data
20+
* @param Error[] $errors
21+
*/
22+
public function __construct(
23+
public ?array $data,
24+
public array $errors = [],
25+
) {
26+
}
27+
28+
/**
29+
* @return array{
30+
* result: ?T,
31+
* errors?: string[],
32+
* }
33+
*/
34+
public function format(): array
35+
{
36+
$output = [
37+
'result' => $this->data,
38+
];
39+
assertType('array{result: T of array (class Bug10445\Response, argument)|null}', $output);
40+
if (count($this->errors) > 0) {
41+
$output['errors'] = array_map(fn ($e) => $e->name, $this->errors);
42+
assertType("array{result: T of array (class Bug10445\Response, argument)|null, errors: non-empty-array<'A'|'B'|'C'>}", $output);
43+
} else {
44+
assertType('array{result: T of array (class Bug10445\Response, argument)|null}', $output);
45+
}
46+
assertType("array{result: T of array (class Bug10445\Response, argument)|null, errors?: non-empty-array<'A'|'B'|'C'>}", $output);
47+
return $output;
48+
}
49+
}

0 commit comments

Comments
 (0)