diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 714b4b311f..68933ecad7 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -147,6 +147,11 @@ public function getValuesArray(): Type return $this; } + public function fillKeysArray(Type $valueType): Type + { + return new MixedType(); + } + public function flipArray(): Type { return new MixedType(); diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 82d72dd85e..0f5236663a 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -136,6 +136,11 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function fillKeysArray(Type $valueType): Type + { + return new NonEmptyArrayType(); + } + public function shuffleArray(): Type { return new NonEmptyArrayType(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index e6710800c8..fab2456091 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -165,6 +165,11 @@ public function getValuesArray(): Type return new NonEmptyArrayType(); } + public function fillKeysArray(Type $valueType): Type + { + return new NonEmptyArrayType(); + } + public function flipArray(): Type { $valueType = $this->valueType->toArrayKey(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 191478c239..8a7995569d 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -137,6 +137,11 @@ public function getValuesArray(): Type return $this; } + public function fillKeysArray(Type $valueType): Type + { + return $this; + } + public function flipArray(): Type { return $this; diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index d62d2dc17a..93533af431 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -136,6 +136,11 @@ public function getValuesArray(): Type return $this; } + public function fillKeysArray(Type $valueType): Type + { + return $this; + } + public function flipArray(): Type { return $this; diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index ce5a953a2e..4532a4cd5a 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -367,6 +367,21 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function fillKeysArray(Type $valueType): Type + { + $itemType = $this->getItemType(); + if ((new IntegerType())->isSuperTypeOf($itemType)->no()) { + $stringKeyType = $itemType->toString(); + if ($stringKeyType instanceof ErrorType) { + return $stringKeyType; + } + + return new ArrayType($stringKeyType, $valueType); + } + + return new ArrayType($itemType, $valueType); + } + public function flipArray(): Type { return new self($this->getIterableValueType()->toArrayKey(), $this->getIterableKeyType()); diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 0248f4ed14..7fd25df741 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -23,6 +23,7 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; @@ -670,6 +671,26 @@ public function unsetOffset(Type $offsetType): Type return new ArrayType($this->getKeyType(), $this->getItemType()); } + public function fillKeysArray(Type $valueType): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($this->valueTypes as $i => $keyType) { + if ((new IntegerType())->isSuperTypeOf($keyType)->no()) { + $stringKeyType = $keyType->toString(); + if ($stringKeyType instanceof ErrorType) { + return $stringKeyType; + } + + $builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i)); + } else { + $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i)); + } + } + + return $builder->getArray(); + } + public function flipArray(): Type { $builder = ConstantArrayTypeBuilder::createEmpty(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 6ad906e24c..40d66a3e4a 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -521,6 +521,11 @@ public function getValuesArray(): Type return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray()); } + public function fillKeysArray(Type $valueType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType)); + } + public function flipArray(): Type { return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray()); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 91ac202260..5787808b11 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -162,6 +162,15 @@ public function getValuesArray(): Type return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed))); } + public function fillKeysArray(Type $valueType): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType($this->getIterableValueType(), $valueType); + } + public function flipArray(): Type { if ($this->isArray()->no()) { diff --git a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php index 5884e9cab8..eb51f2cc54 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php @@ -5,15 +5,8 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\ErrorType; -use PHPStan\Type\IntegerType; -use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; class ArrayFillKeysFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -24,49 +17,13 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_fill_keys'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $valueType = $scope->getType($functionCall->getArgs()[1]->value); - $keysType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = $keysType->getConstantArrays(); - if (count($constantArrays) === 0) { - if ($keysType->isArray()->yes()) { - $itemType = $keysType->getIterableValueType(); - - if ((new IntegerType())->isSuperTypeOf($itemType)->no()) { - if ($itemType->toString() instanceof ErrorType) { - return new ArrayType($itemType, $valueType); - } - - return new ArrayType($itemType->toString(), $valueType); - } - } - - return new ArrayType($keysType->getIterableValueType(), $valueType); - } - - $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getValueTypes() as $i => $keyType) { - if ((new IntegerType())->isSuperTypeOf($keyType)->no()) { - if ($keyType->toString() instanceof ErrorType) { - return new NeverType(); - } - - $arrayBuilder->setOffsetValueType($keyType->toString(), $valueType, $constantArray->isOptionalKey($i)); - } else { - $arrayBuilder->setOffsetValueType($keyType, $valueType, $constantArray->isOptionalKey($i)); - } - } - $arrayTypes[] = $arrayBuilder->getArray(); - } - - return TypeCombinator::union(...$arrayTypes); + return $scope->getType($functionCall->getArgs()[0]->value)->fillKeysArray($scope->getType($functionCall->getArgs()[1]->value)); } } diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index cbf3e47b1e..cb7a699e86 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -366,6 +366,11 @@ public function getValuesArray(): Type return $this->getStaticObjectType()->getValuesArray(); } + public function fillKeysArray(Type $valueType): Type + { + return $this->getStaticObjectType()->fillKeysArray($valueType); + } + public function flipArray(): Type { return $this->getStaticObjectType()->flipArray(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 308700af06..0e73c94828 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -215,6 +215,11 @@ public function getValuesArray(): Type return $this->resolve()->getValuesArray(); } + public function fillKeysArray(Type $valueType): Type + { + return $this->resolve()->fillKeysArray($valueType); + } + public function flipArray(): Type { return $this->resolve()->flipArray(); diff --git a/src/Type/Traits/MaybeArrayTypeTrait.php b/src/Type/Traits/MaybeArrayTypeTrait.php index db74794731..1ee90d8ff3 100644 --- a/src/Type/Traits/MaybeArrayTypeTrait.php +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -49,6 +49,11 @@ public function getValuesArray(): Type return new ErrorType(); } + public function fillKeysArray(Type $valueType): Type + { + return new ErrorType(); + } + public function flipArray(): Type { return new ErrorType(); diff --git a/src/Type/Traits/NonArrayTypeTrait.php b/src/Type/Traits/NonArrayTypeTrait.php index 4efb57bc17..68c88040a7 100644 --- a/src/Type/Traits/NonArrayTypeTrait.php +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -49,6 +49,11 @@ public function getValuesArray(): Type return new ErrorType(); } + public function fillKeysArray(Type $valueType): Type + { + return new ErrorType(); + } + public function flipArray(): Type { return new ErrorType(); diff --git a/src/Type/Type.php b/src/Type/Type.php index b3de8550f1..6ecba3e5ba 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -100,6 +100,8 @@ public function getKeysArray(): Type; public function getValuesArray(): Type; + public function fillKeysArray(Type $valueType): Type; + public function flipArray(): Type; public function popArray(): Type; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 7108a652c0..4be1360067 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -549,6 +549,11 @@ public function getValuesArray(): Type return $this->unionTypes(static fn (Type $type): Type => $type->getValuesArray()); } + public function fillKeysArray(Type $valueType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType)); + } + public function flipArray(): Type { return $this->unionTypes(static fn (Type $type): Type => $type->flipArray()); diff --git a/tests/PHPStan/Analyser/data/array-fill-keys.php b/tests/PHPStan/Analyser/data/array-fill-keys.php index fcf82d1094..e5b0782c24 100644 --- a/tests/PHPStan/Analyser/data/array-fill-keys.php +++ b/tests/PHPStan/Analyser/data/array-fill-keys.php @@ -48,7 +48,7 @@ function withObjectKey() : array { assertType("array{foo: 'b'}", array_fill_keys([new Foo()], 'b')); assertType("non-empty-array", array_fill_keys([new Bar()], 'b')); - assertType("*NEVER*", array_fill_keys([new Baz()], 'b')); + assertType("*ERROR*", array_fill_keys([new Baz()], 'b')); } function withUnionKeys(): void @@ -71,6 +71,10 @@ function withOptionalKeys(): void $arr1[] = 'baz'; } assertType("array{foo: 'b', bar: 'b', baz?: 'b'}", array_fill_keys($arr1, 'b')); + + /** @var array{0?: 'foo', 1: 'bar', }|array{0: 'baz', 1?: 'foobar'} $arr2 */ + $arr2 = []; + assertType("array{baz: 'b', foobar?: 'b'}|array{foo?: 'b', bar: 'b'}", array_fill_keys($arr2, 'b')); } /** @@ -79,12 +83,33 @@ function withOptionalKeys(): void * @param Foo[] $baz * @param float[] $floats * @param array $mixed + * @param list $list + * @param Baz[] $objectsWithoutToString */ -function withNotConstantArray(array $foo, array $bar, array $baz, array $floats, array $mixed): void +function withNotConstantArray(array $foo, array $bar, array $baz, array $floats, array $mixed, array $list, array $objectsWithoutToString): void { assertType("array", array_fill_keys($foo, null)); assertType("array", array_fill_keys($bar, null)); assertType("array<'foo', null>", array_fill_keys($baz, null)); assertType("array", array_fill_keys($floats, null)); assertType("array", array_fill_keys($mixed, null)); + assertType('array', array_fill_keys($list, null)); + assertType('*ERROR*', array_fill_keys($objectsWithoutToString, null)); + + if (array_key_exists(17, $mixed)) { + assertType('non-empty-array', array_fill_keys($mixed, null)); + } + + if (array_key_exists(17, $mixed) && $mixed[17] === 'foo') { + assertType('non-empty-array', array_fill_keys($mixed, null)); + } +} + +function mixedAndSubtractedArray($mixed): void +{ + if (is_array($mixed)) { + assertType("array<'b'>", array_fill_keys($mixed, 'b')); + } else { + assertType("*ERROR*", array_fill_keys($mixed, 'b')); + } }