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
5 changes: 5 additions & 0 deletions src/Type/Accessory/AccessoryArrayListType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Accessory/HasOffsetType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Accessory/HasOffsetValueType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Accessory/NonEmptyArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ public function getValuesArray(): Type
return $this;
}

public function fillKeysArray(Type $valueType): Type
{
return $this;
}

public function flipArray(): Type
{
return $this;
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Accessory/OversizedArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ public function getValuesArray(): Type
return $this;
}

public function fillKeysArray(Type $valueType): Type
{
return $this;
}

public function flipArray(): Type
{
return $this;
Expand Down
15 changes: 15 additions & 0 deletions src/Type/ArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
21 changes: 21 additions & 0 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
9 changes: 9 additions & 0 deletions src/Type/MixedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
49 changes: 3 additions & 46 deletions src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
}

}
5 changes: 5 additions & 0 deletions src/Type/StaticType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Traits/LateResolvableTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Traits/MaybeArrayTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Traits/NonArrayTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions src/Type/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/Type/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
29 changes: 27 additions & 2 deletions tests/PHPStan/Analyser/data/array-fill-keys.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function withObjectKey() : array
{
assertType("array{foo: 'b'}", array_fill_keys([new Foo()], 'b'));
assertType("non-empty-array<string, 'b'>", 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
Expand All @@ -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'));
}

/**
Expand All @@ -79,12 +83,33 @@ function withOptionalKeys(): void
* @param Foo[] $baz
* @param float[] $floats
* @param array<int, int|string|bool> $mixed
* @param list<string> $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<string, null>", array_fill_keys($foo, null));
assertType("array<int, null>", array_fill_keys($bar, null));
assertType("array<'foo', null>", array_fill_keys($baz, null));
assertType("array<numeric-string, null>", array_fill_keys($floats, null));
assertType("array<bool|int|string, null>", array_fill_keys($mixed, null));
assertType('array<string, null>', array_fill_keys($list, null));
assertType('*ERROR*', array_fill_keys($objectsWithoutToString, null));

if (array_key_exists(17, $mixed)) {
assertType('non-empty-array<bool|int|string, null>', array_fill_keys($mixed, null));
}

if (array_key_exists(17, $mixed) && $mixed[17] === 'foo') {
assertType('non-empty-array<bool|int|string, null>', 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'));
}
}