Skip to content

Commit ea3e112

Browse files
authored
Fix union of template array-key with false losing the template (#6376)
1 parent adb28cf commit ea3e112

10 files changed

Lines changed: 310 additions & 7 deletions

File tree

‎src/Type/TypeCombinator.php‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@ public static function doUnion(Type ...$types): Type
256256
$alreadyNormalizedCounter = 0;
257257

258258
$benevolentTypes = [];
259-
$benevolentUnionObject = null;
260259
$neverCount = 0;
261260
// transform A | (B | C) to A | B | C
262261
for ($i = 0; $i < $typesCount; $i++) {
@@ -273,8 +272,8 @@ public static function doUnion(Type ...$types): Type
273272
continue;
274273
}
275274
if ($types[$i] instanceof BenevolentUnionType) {
276-
if ($types[$i] instanceof TemplateBenevolentUnionType && $benevolentUnionObject === null) {
277-
$benevolentUnionObject = $types[$i];
275+
if ($types[$i] instanceof TemplateType) {
276+
continue;
278277
}
279278
$benevolentTypesCount = 0;
280279
$typesInner = $types[$i]->getTypes();
@@ -528,10 +527,6 @@ public static function doUnion(Type ...$types): Type
528527
}
529528

530529
if ($tempTypes === []) {
531-
if ($benevolentUnionObject instanceof TemplateBenevolentUnionType) {
532-
return $benevolentUnionObject->withTypes(array_values($types));
533-
}
534-
535530
return new BenevolentUnionType(array_values($types), true);
536531
}
537532
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug10871;
6+
7+
use stdClass;
8+
use function PHPStan\Testing\assertType;
9+
10+
/**
11+
* @template TKey of array-key
12+
* @template TValue
13+
*/
14+
interface Map
15+
{
16+
17+
/**
18+
* @template TOtherKey of array-key
19+
* @template TOtherValue
20+
* @param iterable<TOtherKey, TOtherValue> ...$iterables
21+
* @return self<TKey|TOtherKey, TValue|TOtherValue>
22+
*/
23+
public function merge(iterable ...$iterables): self;
24+
25+
}
26+
27+
/**
28+
* @param Map<string, int> $map
29+
*/
30+
function test(Map $map, int $int, bool $bool): void
31+
{
32+
assertType('Bug10871\Map<int|string, bool|int>', $map->merge([$int => $bool]));
33+
assertType('Bug10871\Map<int|string, bool|int|stdClass>', $map->merge([$int => $bool], ['test' => new stdClass()]));
34+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug13192;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
class Apple
10+
{
11+
}
12+
13+
class Orange
14+
{
15+
}
16+
17+
/**
18+
* @template TKey of array-key
19+
* @template TValue
20+
*/
21+
class Collection
22+
{
23+
24+
/**
25+
* @template UKey of array-key
26+
* @template UValue
27+
* @param static<UKey, UValue> $items
28+
* @return static<TKey|UKey, TValue|UValue>
29+
*/
30+
public function merge(self $items): static
31+
{
32+
return $this;
33+
}
34+
35+
}
36+
37+
/**
38+
* @param Collection<int, Orange> $oranges
39+
* @param Collection<string, Apple> $apples
40+
*/
41+
function test(Collection $oranges, Collection $apples): void
42+
{
43+
assertType('Bug13192\Collection<int|string, Bug13192\Apple|Bug13192\Orange>', $oranges->merge($apples));
44+
assertType('Bug13192\Collection<int|string, Bug13192\Apple|Bug13192\Orange>', $apples->merge($oranges));
45+
assertType('Bug13192\Collection<int, Bug13192\Orange>', $oranges->merge($oranges));
46+
assertType('Bug13192\Collection<string, Bug13192\Apple>', $apples->merge($apples));
47+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug13374;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template TKey of array-key
11+
* @template TValue
12+
*/
13+
class Collection
14+
{
15+
16+
/**
17+
* @template TPushValue
18+
* @param TPushValue $value
19+
* @return $this
20+
* @phpstan-this-out static<int|TKey, TValue|TPushValue>
21+
*/
22+
public function push(mixed $value): static
23+
{
24+
return $this;
25+
}
26+
27+
/**
28+
* @template TPutKey of array-key
29+
* @template TPutValue
30+
* @param TPutKey $key
31+
* @param TPutValue $value
32+
* @return $this
33+
* @phpstan-this-out static<TKey|TPutKey, TValue|TPutValue>
34+
*/
35+
public function put(int|string $key, mixed $value): static
36+
{
37+
return $this;
38+
}
39+
40+
}
41+
42+
/**
43+
* @param Collection<string, int> $pushCollection
44+
* @param Collection<string, int> $putCollection
45+
*/
46+
function test(Collection $pushCollection, Collection $putCollection): void
47+
{
48+
assertType('Bug13374\Collection<int|string, int>', $pushCollection->push(123));
49+
assertType('Bug13374\Collection<int|string, int|string>', $pushCollection->push('foo'));
50+
assertType('Bug13374\Collection<int|string, int>', $putCollection->put(123, 456));
51+
assertType('Bug13374\Collection<int|string, int|string>', $putCollection->put(789, 'foo'));
52+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug7049;
6+
7+
use Closure;
8+
use function PHPStan\Testing\assertType;
9+
10+
class Collection
11+
{
12+
13+
/**
14+
* @template TGroupKey of array-key
15+
* @param TGroupKey|Closure(mixed): TGroupKey $key
16+
* @return array<TGroupKey, static>
17+
*/
18+
public function groupBy(int|string|Closure $key): array
19+
{
20+
return [];
21+
}
22+
23+
}
24+
25+
$collection = new Collection();
26+
assertType("array<'id', Bug7049\Collection>", $collection->groupBy('id'));
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug7279;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template K of array-key
11+
* @template T
12+
* @param array<K, T> $array
13+
* @param callable(T, K): bool $fn
14+
* @return ($array is non-empty-array ? K|null : null)
15+
*/
16+
function findKey(array $array, callable $fn): string|int|null
17+
{
18+
foreach ($array as $key => $value) {
19+
if ($fn($value, $key)) {
20+
return $key;
21+
}
22+
}
23+
24+
return null;
25+
}
26+
27+
/**
28+
* @param callable(mixed): bool $callback
29+
* @param array<never, never> $emptyList
30+
* @param array{} $emptyMap
31+
* @param array<int, string> $unknownList
32+
* @param array{id?: int, name?: string} $unknownMap
33+
* @param non-empty-array<int, string> $nonEmptyList
34+
* @param array{work: string} $nonEmptyMap
35+
*/
36+
function test(callable $callback, array $emptyList, array $emptyMap, array $unknownList, array $unknownMap, array $nonEmptyList, array $nonEmptyMap): void
37+
{
38+
assertType('null', findKey([], $callback));
39+
assertType('null', findKey($emptyList, $callback));
40+
assertType('null', findKey($emptyMap, $callback));
41+
assertType('int|null', findKey($unknownList, $callback));
42+
assertType("'id'|'name'|null", findKey($unknownMap, $callback));
43+
assertType('int|null', findKey($nonEmptyList, $callback));
44+
assertType("'work'|null", findKey($nonEmptyMap, $callback));
45+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug7423;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template TKey of array-key
11+
* @template TValue
12+
*/
13+
class ArrayType
14+
{
15+
16+
/**
17+
* @template VKey of array-key
18+
* @template V
19+
* @param VKey $key
20+
* @param V $value
21+
* @return self<TKey|VKey, TValue|V>
22+
*/
23+
public function add($key, $value): self
24+
{
25+
return $this;
26+
}
27+
28+
}
29+
30+
/** @var ArrayType<string, string> $type */
31+
$type = new ArrayType();
32+
33+
assertType('Bug7423\ArrayType<int|string, int|string>', $type->add(1, 1));
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug8268;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template TKey of array-key
11+
*/
12+
class Collection
13+
{
14+
15+
/**
16+
* @param TKey|null $offset
17+
*/
18+
public function set(int|string|null $offset): void
19+
{
20+
assertType('TKey of (int|string) (class Bug8268\Collection, argument)|null', $offset);
21+
if ($offset === null) {
22+
return;
23+
}
24+
25+
assertType('TKey of (int|string) (class Bug8268\Collection, argument)', $offset);
26+
}
27+
28+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace TemplateArrayKeyUnionFalse;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template TKey of array-key
9+
* @template TValue
10+
*/
11+
class Collection
12+
{
13+
14+
/**
15+
* @param TValue|callable(TValue, TKey): bool $value
16+
* @return TKey|false
17+
*/
18+
public function search($value, bool $strict = false)
19+
{
20+
return false;
21+
}
22+
23+
}
24+
25+
/**
26+
* @param Collection<int, string> $ints
27+
* @param Collection<string, string> $strings
28+
* @param Collection<int|string, string> $keys
29+
*/
30+
function test(Collection $ints, Collection $strings, Collection $keys): void
31+
{
32+
assertType('int|false', $ints->search('foo'));
33+
assertType('string|false', $strings->search('foo'));
34+
assertType('int|string|false', $keys->search('foo'));
35+
}

‎tests/PHPStan/Type/TypeCombinatorTest.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1982,6 +1982,14 @@ public static function dataUnion(): iterable
19821982
TemplateBenevolentUnionType::class,
19831983
'T of (int|string) (function foo(), parameter)',
19841984
],
1985+
[
1986+
[
1987+
TemplateTypeFactory::create(TemplateTypeScope::createWithFunction('foo'), 'T', new BenevolentUnionType([new IntegerType(), new StringType()]), TemplateTypeVariance::createInvariant()),
1988+
new ConstantBooleanType(false),
1989+
],
1990+
UnionType::class,
1991+
'T of (int|string) (function foo(), parameter)|false',
1992+
],
19851993
[
19861994
[
19871995
new ConstantStringType(''),

0 commit comments

Comments
 (0)