diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 8850f7f45a..2077f69256 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -969,9 +969,9 @@ public function fillKeysArray(Type $valueType): Type return $stringKeyType; } - $builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i)); + $builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i) || count($stringKeyType->getConstantScalarTypes()) > 1); } else { - $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i)); + $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i) || count($keyType->getConstantScalarTypes()) > 1); } } @@ -984,10 +984,11 @@ public function flipArray(): Type foreach ($this->keyTypes as $i => $keyType) { $valueType = $this->valueTypes[$i]; + $offsetType = $valueType->toArrayKey(); $builder->setOffsetValueType( - $valueType->toArrayKey(), + $offsetType, $keyType, - $this->isOptionalKey($i), + $this->isOptionalKey($i) || count($offsetType->getConstantScalarTypes()) > 1, ); } diff --git a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php index c961c45362..7a9474f5c6 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php @@ -7,10 +7,12 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function count; #[AutowiredService] @@ -38,7 +40,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return $keysType->fillKeysArray($scope->getType($args[1]->value)); + $filled = $keysType->fillKeysArray($scope->getType($args[1]->value)); + if ($keysType->isIterableAtLeastOnce()->yes() && $filled->isArray()->yes()) { + return TypeCombinator::intersect($filled, new NonEmptyArrayType()); + } + return $filled; } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14656.php b/tests/PHPStan/Analyser/nsrt/bug-14656.php new file mode 100644 index 0000000000..85c422c558 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14656.php @@ -0,0 +1,47 @@ +", array_flip($a)); + } + + /** @param array{0: 'a'|'b', 1: 'c'} $a */ + public function mixedUnionAndConstant(array $a): void + { + assertType("array{a?: 0, b?: 0, c: 1}", array_flip($a)); + } +} + +class ArrayFillKeysUnionValues +{ + /** @param array{0: 'a'|'b', 1: 'b'|'c'} $a */ + public function overlappingUnion(array $a): void + { + assertType("non-empty-array<'a'|'b'|'c', 'x'>", array_fill_keys($a, 'x')); + } + + /** @param array{0: 'a'|'b'|'c', 1: 'a'|'b'|'c', 2: 'a'|'b'|'c'} $a */ + public function allUnion(array $a): void + { + assertType("non-empty-array{a?: 'x', b?: 'x', c?: 'x'}", array_fill_keys($a, 'x')); + } + + /** @param array{0: 'a'|'b', 1: 'c'} $a */ + public function mixedUnionAndConstant(array $a): void + { + assertType("array{a?: 'x', b?: 'x', c: 'x'}", array_fill_keys($a, 'x')); + } +}