Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 6705ac1

Browse files
authored
Preserve large arrays with same keys through union
1 parent 02071ab commit 6705ac1

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
@@ -1557,7 +1557,7 @@ public function isKeysSupersetOf(self $otherArray): bool
15571557

15581558
public function mergeWith(self $otherArray): self
15591559
{
1560-
// only call this after verifying isKeysSupersetOf
1560+
// only call this after verifying isKeysSupersetOf, or if losing tagged unions is not an issue
15611561
$valueTypes = $this->valueTypes;
15621562
$optionalKeys = $this->optionalKeys;
15631563
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
@@ -1420,6 +1420,9 @@ public function dataFileAsserts(): iterable
14201420
yield from $this->gatherAssertTypes(__DIR__ . '/data/trigger-error-php7.php');
14211421
}
14221422

1423+
yield from $this->gatherAssertTypes(__DIR__ . '/data/preserve-large-constant-array.php');
1424+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9397.php');
1425+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10080.php');
14231426
yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-error-log.php');
14241427
yield from $this->gatherAssertTypes(__DIR__ . '/data/falsy-isset.php');
14251428
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)

AltStyle によって変換されたページ (->オリジナル) /