diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index a594f1a8e6..9ba14f5425 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -6,6 +6,7 @@ use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -674,8 +675,8 @@ public static function intersect(Type ...$types): Type $typesCount = count($types); } - // move subtractables with subtracts before those without to avoid loosing them in the union logic usort($types, static function (Type $a, Type $b): int { + // move subtractables with subtracts before those without to avoid loosing them in the union logic if ($a instanceof SubtractableType && $a->getSubtractedType() !== null) { return -1; } @@ -683,6 +684,13 @@ public static function intersect(Type ...$types): Type return 1; } + if ($a instanceof ConstantArrayType && !$b instanceof ConstantArrayType) { + return -1; + } + if ($b instanceof ConstantArrayType && !$a instanceof ConstantArrayType) { + return 1; + } + return 0; }); @@ -771,6 +779,22 @@ public static function intersect(Type ...$types): Type continue 2; } + if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof ArrayType && !$types[$j] instanceof ConstantArrayType) { + $newArray = ConstantArrayTypeBuilder::createEmpty(); + $valueTypes = $types[$i]->getValueTypes(); + foreach ($types[$i]->getKeyTypes() as $k => $keyType) { + $newArray->setOffsetValueType( + self::intersect($keyType, $types[$j]->getIterableKeyType()), + self::intersect($valueTypes[$k], $types[$j]->getIterableValueType()), + $types[$i]->isOptionalKey($k), + ); + } + $types[$i] = $newArray->getArray(); + array_splice($types, $j--, 1); + $typesCount--; + continue 2; + } + if ( ($types[$i] instanceof ArrayType || $types[$i] instanceof IterableType) && ($types[$j] instanceof ArrayType || $types[$j] instanceof IterableType) diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index fdf67bc2a8..b77b86240d 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -912,6 +912,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7387.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7353.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7031.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-intersect.php'); } /** diff --git a/tests/PHPStan/Analyser/data/constant-array-intersect.php b/tests/PHPStan/Analyser/data/constant-array-intersect.php new file mode 100644 index 0000000000..c1633bb14a --- /dev/null +++ b/tests/PHPStan/Analyser/data/constant-array-intersect.php @@ -0,0 +1,17 @@ + $array1 + * @param array&array{key: string|null} $array2 + */ +function test( + array $array1, + array $array2, +): void { + assertType('array{key: string}', $array1); + assertType('array{key: string}', $array2); +}