diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 22bffb68484..f027c402f05 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1515,10 +1515,14 @@ public static function intersect(Type ...$types): Type $newArray = ConstantArrayTypeBuilder::createEmpty(); $valueTypes = $types[$i]->getValueTypes(); foreach ($types[$i]->getKeyTypes() as $k => $keyType) { + $hasOffset = $types[$j]->hasOffsetValueType($keyType); + if ($hasOffset->no()) { + continue; + } $newArray->setOffsetValueType( self::intersect($keyType, $types[$j]->getIterableKeyType()), - self::intersect($valueTypes[$k], $types[$j]->getIterableValueType()), - $types[$i]->isOptionalKey($k) && !$types[$j]->hasOffsetValueType($keyType)->yes(), + self::intersect($valueTypes[$k], $types[$j]->getOffsetValueType($keyType)), + $types[$i]->isOptionalKey($k) && !$hasOffset->yes(), ); } $types[$i] = $newArray->getArray(); @@ -1531,10 +1535,14 @@ public static function intersect(Type ...$types): Type $newArray = ConstantArrayTypeBuilder::createEmpty(); $valueTypes = $types[$j]->getValueTypes(); foreach ($types[$j]->getKeyTypes() as $k => $keyType) { + $hasOffset = $types[$i]->hasOffsetValueType($keyType); + if ($hasOffset->no()) { + continue; + } $newArray->setOffsetValueType( self::intersect($keyType, $types[$i]->getIterableKeyType()), - self::intersect($valueTypes[$k], $types[$i]->getIterableValueType()), - $types[$j]->isOptionalKey($k) && !$types[$i]->hasOffsetValueType($keyType)->yes(), + self::intersect($valueTypes[$k], $types[$i]->getOffsetValueType($keyType)), + $types[$j]->isOptionalKey($k) && !$hasOffset->yes(), ); } $types[$j] = $newArray->getArray(); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11234.php b/tests/PHPStan/Analyser/nsrt/bug-11234.php new file mode 100644 index 00000000000..bfbcb27e2fa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11234.php @@ -0,0 +1,79 @@ += 8.0 + +namespace Bug11234; + +use function PHPStan\Testing\assertType; + +class Payload {} + +/** @param array{0|1|2|3, int|Payload|string|null}&array{int, Payload} $x */ +function testIntersectConstantUnionWithInt(mixed $x): void +{ + assertType('array{0|1|2|3, Bug11234\Payload}', $x); +} + +/** @param array{int, Payload}&array{0|1|2|3, int|Payload|string|null} $x */ +function testIntersectConstantUnionWithIntReverse(mixed $x): void +{ + assertType('array{0|1|2|3, Bug11234\Payload}', $x); +} + +/** @param array{0|1|2|3, int|Payload|string|null}&array{0|1|2|3, Payload} $x */ +function testIntersectBothConstantUnion(mixed $x): void +{ + assertType('array{0|1|2|3, Bug11234\Payload}', $x); +} + +/** @param array{int, int|Payload|string|null}&array{int, Payload} $y */ +function testIntersectPlainInt(mixed $y): void +{ + assertType('array{int, Bug11234\Payload}', $y); +} + +/** @param array{0|1, string|int, Payload|null}&array{int, string, Payload} $z */ +function testIntersectThreePositions(mixed $z): void +{ + assertType('array{0|1, string, Bug11234\Payload}', $z); +} + +/** @param array{'a'|'b', int|Payload|string|null}&array{string, Payload} $w */ +function testIntersectStringConstantUnion(mixed $w): void +{ + assertType("array{'a'|'b', Bug11234\Payload}", $w); +} + +/** @param array{0|1, int|string}&array{int, int, extra?: bool} $v */ +function testIntersectOptionalKey(mixed $v): void +{ + assertType('array{0|1, int}', $v); +} + +/** @param array{true|false, int|string}&array{bool, string} $u */ +function testIntersectBoolConstantUnion(mixed $u): void +{ + assertType('array{bool, string}', $u); +} + +/** @param array{int<0, 3>, int|Payload|string|null}&array{int, Payload} $x */ +function testIntersectIntRangeValue(mixed $x): void +{ + assertType('array{int<0, 3>, Bug11234\Payload}', $x); +} + +/** @param array{non-empty-string, int|Payload|string|null}&array{string, Payload} $x */ +function testIntersectNonEmptyStringValue(mixed $x): void +{ + assertType('array{non-empty-string, Bug11234\Payload}', $x); +} + +/** @param array{0|1|2|3, non-empty-string|int|null}&array{int, string} $x */ +function testIntersectNonEmptyStringInUnion(mixed $x): void +{ + assertType('array{0|1|2|3, non-empty-string}', $x); +} + +/** @param array{0|1|2|3, string|null}&array{int, non-empty-string} $x */ +function testIntersectWithNonEmptyStringOtherSide(mixed $x): void +{ + assertType('array{0|1|2|3, non-empty-string}', $x); +} diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 6c51dbcd9b1..3607adcddbe 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -4933,6 +4933,24 @@ public static function dataIntersect(): iterable TemplateIntersectionType::class, 'T of Countable&Iterator (function a(), parameter)', ]; + + yield [ + [ + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [ + new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantIntegerType(2), new ConstantIntegerType(3)]), + new UnionType([new IntegerType(), new ObjectType('stdClass'), new StringType(), new NullType()]), + ], + ), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new IntegerType(), new ObjectType('stdClass')], + ), + ], + ConstantArrayType::class, + 'array{0|1|2|3, stdClass}', + ]; } /**