Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -636,16 +636,25 @@ public static function doUnion(Type ...$types): Type
}

if ($benevolentTypes !== []) {
$tempTypes = $types;
foreach ($tempTypes as $i => $type) {
if (!isset($benevolentTypes[$type->describe(VerbosityLevel::value())])) {
break;
$allBenevolent = true;
foreach ($types as $type) {
if (isset($benevolentTypes[$type->describe(VerbosityLevel::value())])) {
continue;
}

// a member that absorbed a benevolent member stays benevolent,
// like int absorbing int<1, max> from (int<1, max>|string) and 0
foreach ($benevolentTypes as $benevolentType) {
if ($type->isSuperTypeOf($benevolentType)->yes()) {
continue 2;
}
}

unset($tempTypes[$i]);
$allBenevolent = false;
break;
}

if ($tempTypes === []) {
if ($allBenevolent) {
return new BenevolentUnionType(array_values($types), true);
}
}
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static function dataFileAsserts(): iterable
yield from self::gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php7.php');
yield from self::gatherAssertTypes(__DIR__ . '/data/bug-11732-php7.php');
yield from self::gatherAssertTypes(__DIR__ . '/data/loose-equality-narrowing-php7.php');
yield from self::gatherAssertTypes(__DIR__ . '/data/bug-15667-php7.php');
}

/**
Expand Down
26 changes: 26 additions & 0 deletions tests/PHPStan/Analyser/data/bug-15667-php7.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php declare(strict_types = 1);

namespace Bug15667Php7;

use function PHPStan\Testing\assertType;

class Foo
{
public function takesString(string $s): void {}

/** @param array<mixed> $fields */
public function doFoo(array $fields): void
{
foreach ($fields as $key => $fieldset) {
assertType('(int|string)', $key);
switch ($key) {
case 'shipping':
break;
case 'billing':
break;
}
assertType('(int|string)', $key);
$this->takesString($key);
}
}
}
8 changes: 4 additions & 4 deletions tests/PHPStan/Analyser/nsrt/array-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,16 +354,16 @@
assertType('1|2', array_rand([1 => 1, 2 => "2"]));
assertType("'a'|'b'", array_rand(["a" => 1, "b" => "2"]));
assertType("2|'a'", array_rand(["a" => 1, 2 => "b"]));
assertType('int|string', array_rand([1 => 1, 2 => "b", $mixed => $mixed]));
assertType('(int|string)', array_rand([1 => 1, 2 => "b", $mixed => $mixed]));
assertType('1|2', array_rand([1 => 1, 2 => "b"], 1));
assertType("'a'|'b'", array_rand(["a" => 1, "b" => "b"], 1));
assertType("2|'a'", array_rand(["a" => 1, 2 => "b"], 1));
assertType('int|string', array_rand([1 => 1, 2 => "b", $mixed => $mixed], 1));
assertType('(int|string)', array_rand([1 => 1, 2 => "b", $mixed => $mixed], 1));
assertType('non-empty-list<1|2>', array_rand([1 => 1, 2 => "b"], 2));
assertType("non-empty-list<'a'|'b'>", array_rand(["a" => 1, "b" => "b"], 2));
assertType("non-empty-list<2|'a'>", array_rand(["a" => 1, 2 => "b"], 2));
assertType('non-empty-list<int|string>', array_rand([1 => 1, 2 => "2", $mixed => $mixed], 2));
assertType('non-empty-list<(int|string)>', array_rand([1 => 1, 2 => "2", $mixed => $mixed], 2));
assertType('1|2|non-empty-list<1|2>', array_rand([1 => 1, 2 => "b"], $mixed));
assertType("'a'|'b'|non-empty-list<'a'|'b'>", array_rand(["a" => 1, "b" => "b"], $mixed));
assertType("2|'a'|non-empty-list<2|'a'>", array_rand(["a" => 1, 2 => "b"], $mixed));
assertType('int|non-empty-list<int|string>|string', array_rand([1 => 1, 2 => "b", $mixed => $mixed], $mixed));
assertType('int|non-empty-list<(int|string)>|string', array_rand([1 => 1, 2 => "b", $mixed => $mixed], $mixed));
16 changes: 16 additions & 0 deletions tests/PHPStan/Type/TypeCombinatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,22 @@ public static function dataUnion(): iterable
BenevolentUnionType::class,
'(int|string)',
],
[
[
new BenevolentUnionType([IntegerRangeType::fromInterval(null, -1), IntegerRangeType::fromInterval(1, null), new StringType()]),
new ConstantIntegerType(0),
],
BenevolentUnionType::class,
'(int|string)',
],
[
[
new BenevolentUnionType([IntegerRangeType::fromInterval(1, null), new StringType()]),
new FloatType(),
],
UnionType::class,
'float|int<1, max>|string',
],
[
[
new UnionType([new IntegerType(), new StringType(), new FloatType()]),
Expand Down
19 changes: 16 additions & 3 deletions turbo-ext/src/TypeCombinator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1289,10 +1289,23 @@ class TypeCombinator
zv::Val description = describe(type.raw(), valueLevel);
zend_string *key = describedKey(description);
if (UNEXPECTED(key == NULL)) return zv::Val();
if (zend_symtable_find(benevolentTypes.table(), key) == NULL) {
allBenevolent = false;
break;
if (zend_symtable_find(benevolentTypes.table(), key) != NULL) continue;

/* a member that absorbed a benevolent member stays benevolent,
* like int absorbing int<1, max> from (int<1, max>|string) and 0 */
bool absorbed = false;
for (zv::ArrayEntry entry : zv::ArrRef(benevolentTypes.raw())) {
int superType = isSuperTypeYes(type.raw(), entry.value().raw());
if (UNEXPECTED(superType < 0)) return zv::Val();
if (superType == 1) {
absorbed = true;
break;
}
}
if (absorbed) continue;

allBenevolent = false;
break;
}

if (allBenevolent) return benevolentUnionType(listOf(types), true);
Expand Down
Loading