Skip to content

Commit b323fc3

Browse files
authored
Merge branch refs/heads/1.10.x into 1.11.x
2 parents c544797 + 6705ac1 commit b323fc3

File tree

6 files changed

+284
-10
lines changed

6 files changed

+284
-10
lines changed

src/Type/Constant/ConstantArrayType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1562,7 +1562,7 @@ public function isKeysSupersetOf(self $otherArray): bool
15621562

15631563
public function mergeWith(self $otherArray): self
15641564
{
1565-
// only call this after verifying isKeysSupersetOf
1565+
// only call this after verifying isKeysSupersetOf, or if losing tagged unions is not an issue
15661566
$valueTypes = $this->valueTypes;
15671567
$optionalKeys = $this->optionalKeys;
15681568
foreach ($this->keyTypes as $i => $keyType) {

src/Type/TypeCombinator.php

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ private static function processArrayAccessoryTypes(array $arrayTypes): array
643643
}
644644

645645
/**
646-
* @param Type[] $arrayTypes
646+
* @param list<Type> $arrayTypes
647647
* @return Type[]
648648
*/
649649
private static function processArrayTypes(array $arrayTypes): array
@@ -669,9 +669,14 @@ private static function processArrayTypes(array $arrayTypes): array
669669

670670
/** @var int|float $nextConstantKeyTypeIndex */
671671
$nextConstantKeyTypeIndex = 1;
672+
$constantArraysMap = array_map(
673+
static fn (Type $t) => $t->getConstantArrays(),
674+
$arrayTypes,
675+
);
672676

673-
foreach ($arrayTypes as $arrayType) {
674-
$isConstantArray = $arrayType->isConstantArray()->yes();
677+
foreach ($arrayTypes as $arrayIdx => $arrayType) {
678+
$constantArrays = $constantArraysMap[$arrayIdx];
679+
$isConstantArray = $constantArrays !== [];
675680
if (!$isConstantArray || !$arrayType->isIterableAtLeastOnce()->no()) {
676681
$filledArrays++;
677682
}
@@ -708,6 +713,10 @@ private static function processArrayTypes(array $arrayTypes): array
708713
}
709714

710715
if ($generalArrayOccurred && (!$overflowed || $filledArrays > 1)) {
716+
$reducedArrayTypes = self::reduceArrays($arrayTypes, false);
717+
if (count($reducedArrayTypes) === 1) {
718+
return [self::intersect($reducedArrayTypes[0], ...$accessoryTypes)];
719+
}
711720
$scopes = [];
712721
$useTemplateArray = true;
713722
foreach ($arrayTypes as $arrayType) {
@@ -740,7 +749,7 @@ private static function processArrayTypes(array $arrayTypes): array
740749
];
741750
}
742751

743-
$reducedArrayTypes = self::reduceArrays($arrayTypes);
752+
$reducedArrayTypes = self::reduceArrays($arrayTypes, true);
744753

745754
return array_map(
746755
static fn (Type $arrayType) => self::intersect($arrayType, ...$accessoryTypes),
@@ -833,16 +842,21 @@ private static function countConstantArrayValueTypes(array $types): int
833842
}
834843

835844
/**
836-
* @param Type[] $constantArrays
837-
* @return Type[]
845+
* @param list<Type> $constantArrays
846+
* @return list<Type>
838847
*/
839-
private static function reduceArrays(array $constantArrays): array
848+
private static function reduceArrays(array $constantArrays, bool $preserveTaggedUnions): array
840849
{
841850
$newArrays = [];
842851
$arraysToProcess = [];
843852
$emptyArray = null;
844853
foreach ($constantArrays as $constantArray) {
845854
if (!$constantArray->isConstantArray()->yes()) {
855+
// This is an optimization for current use-case of $preserveTaggedUnions=false, where we need
856+
// one constant array as a result, or we generalize the $constantArrays.
857+
if (!$preserveTaggedUnions) {
858+
return $constantArrays;
859+
}
846860
$newArrays[] = $constantArray;
847861
continue;
848862
}
@@ -888,7 +902,8 @@ private static function reduceArrays(array $constantArrays): array
888902
}
889903

890904
if (
891-
$overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
905+
$preserveTaggedUnions
906+
&& $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
892907
&& $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i])
893908
) {
894909
$arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
@@ -897,13 +912,25 @@ private static function reduceArrays(array $constantArrays): array
897912
}
898913

899914
if (
900-
$overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
915+
$preserveTaggedUnions
916+
&& $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
901917
&& $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j])
902918
) {
903919
$arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]);
904920
unset($arraysToProcess[$j]);
905921
continue 1;
906922
}
923+
924+
if (
925+
!$preserveTaggedUnions
926+
// both arrays have same keys
927+
&& $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
928+
&& $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
929+
) {
930+
$arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
931+
unset($arraysToProcess[$i]);
932+
continue 2;
933+
}
907934
}
908935
}
909936

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,9 @@ public function dataFileAsserts(): iterable
14281428
yield from $this->gatherAssertTypes(__DIR__ . '/data/trigger-error-php7.php');
14291429
}
14301430

1431+
yield from $this->gatherAssertTypes(__DIR__ . '/data/preserve-large-constant-array.php');
1432+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9397.php');
1433+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10080.php');
14311434
yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-error-log.php');
14321435
yield from $this->gatherAssertTypes(__DIR__ . '/data/falsy-isset.php');
14331436
yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-coalesce.php');
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace Bug10080;
4+
5+
/**
6+
* @param array{
7+
* a1?: string,
8+
* a2?: string,
9+
* a3?: string,
10+
* a4?: string,
11+
* a5?: string,
12+
* a6?: string,
13+
* a7?: string,
14+
* a8?: string,
15+
* a9?: string,
16+
* a10?: string,
17+
* a11?: string,
18+
* a12?: string,
19+
* a13?: string,
20+
* a14?: string,
21+
* a15?: string,
22+
* a16?: string,
23+
* a17?: string,
24+
* a18?: string,
25+
* a19?: string,
26+
* a20?: string,
27+
* a21?: string,
28+
* a22?: string,
29+
* a23?: string,
30+
* a24?: string,
31+
* a25?: string,
32+
* a26?: string,
33+
* a27?: string,
34+
* a28?: string,
35+
* a29?: string,
36+
* a30?: string,
37+
* a31?: string,
38+
* a32?: string,
39+
* a33?: string,
40+
* a34?: string,
41+
* a35?: string,
42+
* a36?: string,
43+
* a37?: string,
44+
* a38?: string,
45+
* a39?: string,
46+
* a40?: string,
47+
* a41?: string,
48+
* a42?: string,
49+
* a43?: string,
50+
* a44?: string,
51+
* a45?: string,
52+
* a46?: string,
53+
* a47?: string,
54+
* a48?: string,
55+
* a49?: string,
56+
* a50?: string,
57+
* a51?: string,
58+
* a52?: string,
59+
* a53?: string,
60+
* a54?: string,
61+
* a55?: string,
62+
* a56?: string,
63+
* a57?: string,
64+
* a58?: string,
65+
* a59?: string,
66+
* a60?: string,
67+
* a61?: string,
68+
* a62?: string|string[]|int|float,
69+
* a63?: string
70+
* } $row
71+
*/
72+
function doStuff(array $row): void
73+
{
74+
\PHPStan\Testing\assertType('string', $row['a51'] ?? '');
75+
\PHPStan\Testing\assertType('string', $row['a51'] ?? '');
76+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9397;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
final class Money {
8+
public static function zero(): Money {
9+
return new Money();
10+
}
11+
}
12+
13+
14+
class HelloWorld
15+
{
16+
/**
17+
* @return array<int, array{
18+
* foo1: Money,
19+
* foo2: ?Money,
20+
* foo3: string,
21+
* foo4: string,
22+
* foo5: string,
23+
* foo6: string,
24+
* foo7: string,
25+
* foo8: string,
26+
* foo9: string,
27+
* foo10:string,
28+
* foo11:int,
29+
* foo12:int,
30+
* foo13:int,
31+
* foo14:int,
32+
* foo15:int,
33+
* foo16:int,
34+
* foo17:int,
35+
* foo18:int,
36+
* foo19:int,
37+
* foo20:int,
38+
* foo21:bool,
39+
* foo22:bool,
40+
* foo23:bool,
41+
* foo24:bool,
42+
* foo25:bool,
43+
* foo26:bool,
44+
* foo27:bool,
45+
* foo28:bool,
46+
* foo29:bool,
47+
* foo30:bool,
48+
* foo31:bool,
49+
* foo32:string,
50+
* foo33:string,
51+
* foo34:string,
52+
* foo35:string,
53+
* foo36:string,
54+
* foo37:string,
55+
* foo38:string,
56+
* foo39:string,
57+
* foo40:string,
58+
* foo41:string,
59+
* foo42:string,
60+
* foo43:string,
61+
* foo44:string,
62+
* foo45:string,
63+
* foo46:string,
64+
* foo47:string,
65+
* foo48:string,
66+
* foo49:string,
67+
* foo50:string,
68+
* foo51:string,
69+
* foo52:string,
70+
* foo53:string,
71+
* foo54:string,
72+
* foo55:string,
73+
* foo56:string,
74+
* foo57:string,
75+
* foo58:string,
76+
* foo59:string,
77+
* foo60:string,
78+
* foo61:string,
79+
* foo62:string,
80+
* foo63:string,
81+
* }>
82+
* If the above type has 63 or more properties, the bug occurs
83+
*/
84+
private static function callable(): array {
85+
return [];
86+
}
87+
88+
public function callsite(): void {
89+
$result = self::callable();
90+
foreach ($result as $id => $p) {
91+
assertType(Money::class, $p['foo1']);
92+
assertType(Money::class . '|null', $p['foo2']);
93+
assertType('string', $p['foo3']);
94+
95+
$baseDeposit = $p['foo2'] ?? Money::zero();
96+
assertType(Money::class, $p['foo1']);
97+
assertType(Money::class . '|null', $p['foo2']);
98+
assertType('string', $p['foo3']);
99+
}
100+
}
101+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PreserveLargeConstantArray;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @param array{1: string|null, 2: int|null, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float} $arr
9+
*/
10+
function multiKeys(array $arr): void
11+
{
12+
if ($arr[1] !== null && $arr[2] !== null) {
13+
$val = 1;
14+
} elseif ($arr[1] === null && $arr[2] === null) {
15+
$val = 2;
16+
} else {
17+
return;
18+
}
19+
20+
assertType('array{1: string|null, 2: int|null, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float}', $arr);
21+
echo 1;
22+
}
23+
24+
/**
25+
* @param array{1: string|null, 2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float} $arr
26+
*/
27+
function simpleUnion(array $arr): void
28+
{
29+
$val = $arr[1] !== null
30+
? $arr[1]
31+
: null;
32+
assertType('array{1: string|null, 2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float}', $arr);
33+
echo 1;
34+
}
35+
36+
/**
37+
* @param array{1?: string, 2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float} $arr
38+
*/
39+
function optionalKey(array $arr): void
40+
{
41+
$val = isset($arr[1])
42+
? $arr[1]
43+
: null;
44+
assertType('array{1?: string, 2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float}', $arr);
45+
echo 1;
46+
}
47+
48+
/**
49+
* @param array{1: string, 2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float} $arr
50+
*/
51+
function multipleOptions(array $arr): void
52+
{
53+
if ($arr[1] === 'a') {
54+
$brr = $arr;
55+
$brr[1] = 'b';
56+
} elseif ($arr[1] === 'b') {
57+
$brr = $arr;
58+
$brr[1] = 'c';
59+
} elseif ($arr[1] === 'c') {
60+
$brr = $arr;
61+
$brr[1] = 'd';
62+
} else {
63+
$brr = $arr;
64+
}
65+
assertType('array{1: string, 2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float}', $brr);
66+
echo 1;
67+
}

0 commit comments

Comments
 (0)