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 40043e9

Browse files
Tagged unions
Closes phpstan/phpstan#6469
1 parent 2a44c50 commit 40043e9

16 files changed

+253
-37
lines changed

‎src/Type/Constant/ConstantArrayType.php‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1126,12 +1126,18 @@ public function isKeysSupersetOf(self $otherArray): bool
11261126
}
11271127

11281128
$otherKeys = $otherArray->keyTypes;
1129-
foreach ($this->keyTypes as $keyType) {
1129+
foreach ($this->keyTypes as $i => $keyType) {
11301130
foreach ($otherArray->keyTypes as $j => $otherKeyType) {
11311131
if (!$keyType->equals($otherKeyType)) {
11321132
continue;
11331133
}
11341134

1135+
$valueType = $this->valueTypes[$i];
1136+
$otherValueType = $otherArray->valueTypes[$j];
1137+
if ($valueType->isSuperTypeOf($otherValueType)->no()) {
1138+
continue;
1139+
}
1140+
11351141
unset($otherKeys[$j]);
11361142
continue 2;
11371143
}

‎tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3056,7 +3056,7 @@ public function dataBinaryOperations(): array
30563056
"sprintf('%s %s', 'foo', 'bar')",
30573057
],
30583058
[
3059-
'array{}|array{0: \'password\'|\'username\', 1?: \'password\'}',
3059+
'array{}|array{\'password\'}|array{0: \'username\', 1?: \'password\'}',
30603060
'$coalesceArray',
30613061
],
30623062
[
@@ -4571,7 +4571,7 @@ public function dataArrayFunctions(): array
45714571
'array_intersect_key($integers, [])',
45724572
],
45734573
[
4574-
'array{1|4, 2|5, 3|6}',
4574+
'array{1, 2, 3}|array{4, 5, 6}',
45754575
'array_intersect_key(...[$integers, [4, 5, 6]])',
45764576
],
45774577
[
@@ -5389,7 +5389,7 @@ public function dataFunctions(): array
53895389
'$strSplitConstantStringWithInvalidSplitLengthType',
53905390
],
53915391
[
5392-
'array{\'a\'|\'g\', \'b\'|\'h\', \'c\'|\'i\', \'d\'|\'j\', \'e\'|\'k\', \'f\'|\'l\'}',
5392+
"array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}",
53935393
'$strSplitConstantStringWithVariableStringAndConstantSplitLength',
53945394
],
53955395
[
@@ -5632,7 +5632,7 @@ public function dataRangeFunction(): array
56325632
'range(1, doFoo() ? 1 : 2)',
56335633
],
56345634
[
5635-
'array{0: -1|1, 1?: 0|2, 2?: 1, 3?: 2}',
5635+
'array{0: -1, 1: 0, 2: 1, 3?: 2}|array{0: 1, 1?: 2}',
56365636
'range(doFoo() ? -1 : 1, doFoo() ? 1 : 2)',
56375637
],
56385638
[
@@ -8347,19 +8347,19 @@ public function dataIsset(): array
83478347
'$array[\'b\']',
83488348
],
83498349
[
8350-
'array{a: 1|2|3, b: 2|3, c?: 4}',
8350+
'array{a: 1, b: 2}|array{a: 3, b: 3, c: 4}',
83518351
'$array',
83528352
],
83538353
[
8354-
'array{a: 1|2|3, b: 2|3|null, c?: 4}',
8354+
'array{a: 1, b: 2}|array{a: 3, b: 3, c: 4}|array{a: 3, b: null}',
83558355
'$arrayCopy',
83568356
],
83578357
[
8358-
'array{a: 1|2|3, c?: 4}',
8358+
'array{a: 2}',
83598359
'$anotherArrayCopy',
83608360
],
83618361
[
8362-
'array{a: 1|2|3, b?: 2|3|null, c?: 4}',
8362+
'array{a: 1, b: 2}|array{a: 2}|array{a: 3, b: 3, c: 4}|array{a: 3, b: null}',
83638363
'$yetAnotherArrayCopy',
83648364
],
83658365
[
@@ -8391,15 +8391,15 @@ public function dataIsset(): array
83918391
'$lookup[$a] ?? false',
83928392
],
83938393
[
8394-
'\'foo\'|false',
8394+
'\'foo\'',
83958395
'$nullableArray[\'a\'] ?? false',
83968396
],
83978397
[
83988398
'\'bar\'',
83998399
'$nullableArray[\'b\'] ?? false',
84008400
],
84018401
[
8402-
'\'baz\'|false',
8402+
'\'baz\'',
84038403
'$nullableArray[\'c\'] ?? false',
84048404
],
84058405
];
@@ -8777,7 +8777,7 @@ public function dataPhp74Functions(): array
87778777
'$mbStrSplitConstantStringWithInvalidSplitLengthType',
87788778
],
87798779
[
8780-
'array{\'a\'|\'g\', \'b\'|\'h\', \'c\'|\'i\', \'d\'|\'j\', \'e\'|\'k\', \'f\'|\'l\'}',
8780+
"array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}",
87818781
'$mbStrSplitConstantStringWithVariableStringAndConstantSplitLength',
87828782
],
87838783
[
@@ -8833,7 +8833,7 @@ public function dataPhp74Functions(): array
88338833
'$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding',
88348834
],
88358835
[
8836-
'array{\'a\'|\'g\', \'b\'|\'h\', \'c\'|\'i\', \'d\'|\'j\', \'e\'|\'k\', \'f\'|\'l\'}',
8836+
"array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}",
88378837
'$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding',
88388838
],
88398839
[

‎tests/PHPStan/Analyser/NodeScopeResolverTest.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,7 @@ public function dataFileAsserts(): iterable
999999
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-constant.php');
10001000
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-constant.php');
10011001
yield from $this->gatherAssertTypes(__DIR__ . '/data/composer-array-bug.php');
1002+
yield from $this->gatherAssertTypes(__DIR__ . '/data/tagged-unions.php');
10021003
}
10031004

10041005
/**

‎tests/PHPStan/Analyser/data/array-merge2.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public function arrayMergeUnionType($array1, $array2): void
5252
public function arrayMergeUnionTypeArrayShapes($array1, $array2): void
5353
{
5454
assertType("array<int, array{bar: '2'}|array{foo: '1'}>", array_merge($array1, $array1));
55-
assertType("array<int, array{bar: '2'|'3'}|array{foo: '1'|'2'}>", array_merge($array1, $array2));
56-
assertType("array<int, array{bar: '2'|'3'}|array{foo: '1'|'2'}>", array_merge($array2, $array1));
55+
assertType("array<int, array{bar: '2'}|array{bar: '3'}|array{foo: '1'}|array{foo: '2'}>", array_merge($array1, $array2));
56+
assertType("array<int, array{bar: '2'}|array{bar: '3'}|array{foo: '1'}|array{foo: '2'}>", array_merge($array2, $array1));
5757
}
5858
}

‎tests/PHPStan/Analyser/data/array-replace.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function arrayReplaceUnionType($array1, $array2): void
6565
public function arrayReplaceUnionTypeArrayShapes($array1, $array2): void
6666
{
6767
assertType("array<int, array{bar: '2'}|array{foo: '1'}>", array_replace($array1, $array1));
68-
assertType("array<int, array{bar: '2'|'3'}|array{foo: '1'|'2'}>", array_replace($array1, $array2));
69-
assertType("array<int, array{bar: '2'|'3'}|array{foo: '1'|'2'}>", array_replace($array2, $array1));
68+
assertType("array<int, array{bar: '2'}|array{bar: '3'}|array{foo: '1'}|array{foo: '2'}>", array_replace($array1, $array2));
69+
assertType("array<int, array{bar: '2'}|array{bar: '3'}|array{foo: '1'}|array{foo: '2'}>", array_replace($array2, $array1));
7070
}
7171
}

‎tests/PHPStan/Analyser/data/bug-3269.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ public static function bar(array $intervalGroups): void
2020
}
2121
}
2222

23-
assertType('array<int, array{version: string, operator: string, side: \'end\'|\'start\'}>', $borders);
23+
assertType("array<int, array{version: string, operator: string, side: 'end'}|array{version: string, operator: string, side: 'start'}>", $borders);
2424

2525
foreach ($borders as $border) {
26-
assertType('array{version: string, operator: string, side: \'end\'|\'start\'}', $border);
26+
assertType("array{version: string, operator: string, side: 'end'}|array{version: string, operator: string, side: 'start'}", $border);
2727
assertType('\'end\'|\'start\'', $border['side']);
2828
}
2929
}

‎tests/PHPStan/Analyser/data/bug-6383.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function doFoo(string $country): void {
2727

2828
foreach ($options as $key => $option) {
2929
if (isset($option['only_in_country'])) {
30-
assertType("array{value: 'a'|'b'|'c', checked: false, only_in_country: array{0: 'BE'|'DE', 1?: 'CH', 2?: 'DE', 3?: 'DK', 4?: 'FR', 5?: 'NL', 6?: 'SE'}}", $option);
30+
assertType("array{value: 'a', checked: false, only_in_country: array{'DE'}}|array{value: 'b', checked: false, only_in_country: array{'BE', 'CH', 'DE', 'DK', 'FR', 'NL', 'SE'}}", $option);
3131
continue;
3232
}
3333
}

‎tests/PHPStan/Analyser/data/bug-6936-limit.php‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@ public function testLimits():void
3636
$arr[] = 'g';
3737
}
3838

39-
assertType("array{0: 1|'a'|'b'|'c'|'d'|'e'|'f'|'g', 1: 2|'b'|'c'|'d'|'e'|'f'|'g', 2: 3|'c'|'d'|'e'|'f'|'g', 3?: 'd'|'e'|'f'|'g', 4?: 'e'|'f'|'g', 5?: 'f'|'g', 6?: 'g'}", $arr + $arr2);
39+
assertType("array{'e', 2|'f'|'g', 3|'g'}|array{'f', 2|'g', 3}|array{'g', 2, 3}|array{0: 'a', 1: 2|'b'|'c'|'d'|'e'|'f'|'g', 2: 3|'c'|'d'|'e'|'f'|'g', 3?: 'd'|'e'|'f'|'g', 4?: 'e'|'f'|'g', 5?: 'f'|'g', 6?: 'g'}|array{0: 'b', 1: 2|'c'|'d'|'e'|'f'|'g', 2: 3|'d'|'e'|'f'|'g', 3?: 'e'|'f'|'g', 4?: 'f'|'g', 5?: 'g'}|array{0: 'c', 1: 2|'d'|'e'|'f'|'g', 2: 3|'e'|'f'|'g', 3?: 'f'|'g', 4?: 'g'}|array{0: 'd', 1: 2|'e'|'f'|'g', 2: 3|'f'|'g', 3?: 'g'}|array{1, 2, 3}", $arr + $arr2);
4040
if (rand(0,1)) {
4141
$arr[] = 'h';
4242
}
4343

44-
assertType("array{0: 1|'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h', 1: 2|'b'|'c'|'d'|'e'|'f'|'g'|'h', 2: 3|'c'|'d'|'e'|'f'|'g'|'h', 3?: 'd'|'e'|'f'|'g'|'h', 4?: 'e'|'f'|'g'|'h', 5?: 'f'|'g'|'h', 6?: 'g'|'h', 7?: 'h'}", $arr + $arr2);
44+
assertType("array{'f', 2|'g'|'h', 3|'h'}|array{'g', 2|'h', 3}|array{'h', 2, 3}|array{0: 'a', 1: 2|'b'|'c'|'d'|'e'|'f'|'g'|'h', 2: 3|'c'|'d'|'e'|'f'|'g'|'h', 3?: 'd'|'e'|'f'|'g'|'h', 4?: 'e'|'f'|'g'|'h', 5?: 'f'|'g'|'h', 6?: 'g'|'h', 7?: 'h'}|array{0: 'b', 1: 2|'c'|'d'|'e'|'f'|'g'|'h', 2: 3|'d'|'e'|'f'|'g'|'h', 3?: 'e'|'f'|'g'|'h', 4?: 'f'|'g'|'h', 5?: 'g'|'h', 6?: 'h'}|array{0: 'c', 1: 2|'d'|'e'|'f'|'g'|'h', 2: 3|'e'|'f'|'g'|'h', 3?: 'f'|'g'|'h', 4?: 'g'|'h', 5?: 'h'}|array{0: 'd', 1: 2|'e'|'f'|'g'|'h', 2: 3|'f'|'g'|'h', 3?: 'g'|'h', 4?: 'h'}|array{0: 'e', 1: 2|'f'|'g'|'h', 2: 3|'g'|'h', 3?: 'h'}|array{1, 2, 3}", $arr + $arr2);
4545
if (rand(0,1)) {
4646
$arr[] = 'i';
4747
}
4848

49-
assertType("array{0: 1|'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i', 1: 2|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i', 2: 3|'c'|'d'|'e'|'f'|'g'|'h'|'i', 3?: 'd'|'e'|'f'|'g'|'h'|'i', 4?: 'e'|'f'|'g'|'h'|'i', 5?: 'f'|'g'|'h'|'i', 6?: 'g'|'h'|'i', 7?: 'h'|'i', 8?: 'i'}", $arr + $arr2);
49+
assertType("array{'g', 2|'h'|'i', 3|'i'}|array{'h', 2|'i', 3}|array{'i', 2, 3}|array{0: 'a', 1: 2|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i', 2: 3|'c'|'d'|'e'|'f'|'g'|'h'|'i', 3?: 'd'|'e'|'f'|'g'|'h'|'i', 4?: 'e'|'f'|'g'|'h'|'i', 5?: 'f'|'g'|'h'|'i', 6?: 'g'|'h'|'i', 7?: 'h'|'i', 8?: 'i'}|array{0: 'b', 1: 2|'c'|'d'|'e'|'f'|'g'|'h'|'i', 2: 3|'d'|'e'|'f'|'g'|'h'|'i', 3?: 'e'|'f'|'g'|'h'|'i', 4?: 'f'|'g'|'h'|'i', 5?: 'g'|'h'|'i', 6?: 'h'|'i', 7?: 'i'}|array{0: 'c', 1: 2|'d'|'e'|'f'|'g'|'h'|'i', 2: 3|'e'|'f'|'g'|'h'|'i', 3?: 'f'|'g'|'h'|'i', 4?: 'g'|'h'|'i', 5?: 'h'|'i', 6?: 'i'}|array{0: 'd', 1: 2|'e'|'f'|'g'|'h'|'i', 2: 3|'f'|'g'|'h'|'i', 3?: 'g'|'h'|'i', 4?: 'h'|'i', 5?: 'i'}|array{0: 'e', 1: 2|'f'|'g'|'h'|'i', 2: 3|'g'|'h'|'i', 3?: 'h'|'i', 4?: 'i'}|array{0: 'f', 1: 2|'g'|'h'|'i', 2: 3|'h'|'i', 3?: 'i'}|array{1, 2, 3}", $arr + $arr2);
5050
}
5151
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<?php
2+
3+
namespace TaggedUnions;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/** @param array{A: int}|array{A: string} $foo */
11+
public function doFoo(array $foo)
12+
{
13+
assertType('array{A: int}|array{A: string}', $foo);
14+
if (is_int($foo['A'])) {
15+
assertType("array{A: int}", $foo);
16+
$foo['B'] = 'yo';
17+
assertType("array{A: int, B: 'yo'}", $foo);
18+
} else {
19+
assertType('array{A: string}', $foo);
20+
}
21+
22+
assertType("array{A: int, B: 'yo'}|array{A: string}", $foo);
23+
}
24+
25+
/** @param array{A: int, B: 1}|array{A: string, B: 2} $foo */
26+
public function doFoo2(array $foo)
27+
{
28+
assertType('array{A: int, B: 1}|array{A: string, B: 2}', $foo);
29+
if (is_int($foo['A'])) {
30+
assertType("array{A: int, B: 1}", $foo);
31+
} else {
32+
assertType("array{A: string, B: 2}", $foo);
33+
}
34+
35+
assertType('array{A: int, B: 1}|array{A: string, B: 2}', $foo);
36+
}
37+
38+
/** @param array{A: int, B: 1}|array{A: string, C: 1} $foo */
39+
public function doFoo3(array $foo)
40+
{
41+
assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo);
42+
if (is_int($foo['A'])) {
43+
assertType("array{A: int, B: 1}", $foo);
44+
} else {
45+
assertType("array{A: string, C: 1}", $foo);
46+
}
47+
48+
assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo);
49+
}
50+
51+
/** @param array{A: int, B: 1}|array{A: string, C: 1} $foo */
52+
public function doFoo4(array $foo)
53+
{
54+
assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo);
55+
if (isset($foo['C'])) {
56+
assertType("array{A: string, C: 1}", $foo);
57+
} else {
58+
assertType("array{A: int, B: 1}|array{A: string, C: 1}", $foo); // could be array{A: int, B: 1}
59+
}
60+
61+
assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo);
62+
}
63+
64+
/**
65+
* @param array{A: int}|array{A: int|string} $foo
66+
* @return void
67+
*/
68+
public function doBar(array $foo)
69+
{
70+
assertType('array{A: int|string}', $foo);
71+
}
72+
73+
}
74+
75+
/**
76+
* @phpstan-type Topping string
77+
* @phpstan-type Salsa string
78+
*
79+
* @phpstan-type Pizza array{type: 'pizza', toppings: Topping[]}
80+
* @phpstan-type Pasta array{type: 'pasta', salsa: Salsa}
81+
* @phpstan-type Meal Pizza|Pasta
82+
*/
83+
class Test
84+
{
85+
/**
86+
* @param Meal $meal
87+
*/
88+
function test($meal): void {
89+
assertType("array{type: 'pasta', salsa: string}|array{type: 'pizza', toppings: array<string>}", $meal);
90+
if ($meal['type'] === 'pizza') {
91+
assertType("array{type: 'pizza', toppings: array<string>}", $meal);
92+
} else {
93+
assertType("array{type: 'pasta', salsa: string}", $meal);
94+
}
95+
assertType("array{type: 'pasta', salsa: string}|array{type: 'pizza', toppings: array<string>}", $meal);
96+
}
97+
}
98+
99+
class HelloWorld
100+
{
101+
/**
102+
* @return array{updated: true, id: int}|array{updated: false, id: null}
103+
*/
104+
public function sayHello(): array
105+
{
106+
return ['updated' => false, 'id' => 5];
107+
}
108+
109+
public function doFoo()
110+
{
111+
$x = $this->sayHello();
112+
assertType("array{updated: false, id: null}|array{updated: true, id: int}", $x);
113+
if ($x['updated']) {
114+
assertType('array{updated: true, id: int}', $x);
115+
}
116+
}
117+
}
118+
119+
/**
120+
* @psalm-type A array{tag: 'A', foo: bool}
121+
* @psalm-type B array{tag: 'B'}
122+
*/
123+
class X {
124+
/** @psalm-param A|B $arr */
125+
public function ooo(array $arr): void {
126+
assertType("array{tag: 'A', foo: bool}|array{tag: 'B'}", $arr);
127+
if ($arr['tag'] === 'A') {
128+
assertType("array{tag: 'A', foo: bool}", $arr);
129+
} else {
130+
assertType("array{tag: 'B'}", $arr);
131+
}
132+
assertType("array{tag: 'A', foo: bool}|array{tag: 'B'}", $arr);
133+
}
134+
}
135+
136+
class TipsFromArnaud
137+
{
138+
139+
// https://github.com/phpstan/phpstan/issues/7666#issuecomment-1191563801
140+
141+
/**
142+
* @param array{a: int}|array{a: int} $a
143+
*/
144+
public function doFoo(array $a): void
145+
{
146+
assertType('array{a: int}', $a);
147+
}
148+
149+
/**
150+
* @param array{a: int}|array{a: string} $a
151+
*/
152+
public function doFoo2(array $a): void
153+
{
154+
// could be: array{a: int|string}
155+
assertType('array{a: int}|array{a: string}', $a);
156+
}
157+
158+
/**
159+
* @param array{a: int, b: int}|array{a: string, b: string} $a
160+
*/
161+
public function doFoo3(array $a): void
162+
{
163+
assertType('array{a: int, b: int}|array{a: string, b: string}', $a);
164+
}
165+
166+
/**
167+
* @param array{a: int, b: string}|array{a: string, b:string} $a
168+
*/
169+
public function doFoo4(array $a): void
170+
{
171+
// could be: array{a: int|string, b: string}
172+
assertType('array{a: int, b: string}|array{a: string, b: string}', $a);
173+
}
174+
175+
/**
176+
* @param array{a: int, b: string, c: string}|array{a: string, b: string, c: string} $a
177+
*/
178+
public function doFoo5(array $a): void
179+
{
180+
// could be: array{a: int|string, b: string, c: string}
181+
assertType('array{a: int, b: string, c: string}|array{a: string, b: string, c: string}', $a);
182+
}
183+
184+
}

‎tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public function testRule(): void
8484
145,
8585
],
8686
[
87-
'Offset \'c\' does not exist on array{c: bool}|array{e: true}.',
87+
'Offset \'c\' does not exist on array{c: false}|array{c: true}|array{e: true}.',
8888
171,
8989
],
9090
[

0 commit comments

Comments
(0)

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