diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 4f2a5ab1aa..435fd9f2ca 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -172,6 +172,11 @@ public function unsetOffset(Type $offsetType): Type return new ErrorType(); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return $this; diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 42d0178921..da6c176655 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -352,6 +352,11 @@ public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType return new BooleanType(); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return new NonEmptyArrayType(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 7adcd66148..5c60ac764c 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -195,6 +195,11 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return new NonEmptyArrayType(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index be9dcd817b..ae4db44bec 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -159,6 +159,11 @@ public function unsetOffset(Type $offsetType): Type return new ErrorType(); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return $this; diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index 1f0918a760..3b56b813e0 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -154,6 +154,11 @@ public function unsetOffset(Type $offsetType): Type return new ErrorType(); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return $this; diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index e8d812b33f..413c5c6ec3 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -170,6 +170,11 @@ public function generalizeValues(): self return new self($this->keyType, $this->itemType->generalize(GeneralizePrecision::lessSpecific())); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return TypeCombinator::intersect(new self(new IntegerType(), $this->getIterableKeyType()), new AccessoryArrayListType()); diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index a21d176835..9bfd5cd364 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -35,6 +35,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\IsSuperTypeOfResult; use PHPStan\Type\MixedType; @@ -1372,6 +1373,19 @@ private function degradeToGeneralArray(): Type return $builder->getArray(); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + $keysArray = $this->getKeysOrValuesArray($this->keyTypes); + + return TypeCombinator::intersect( + new ArrayType( + new IntegerType(), + $keysArray->getIterableValueType(), + ), + new AccessoryArrayListType(), + ); + } + public function getKeysArray(): self { return $this->getKeysOrValuesArray($this->keyTypes); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 085e0b636f..bf8d74298c 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -842,6 +842,11 @@ public function unsetOffset(Type $offsetType): Type return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict)); + } + public function getKeysArray(): Type { return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray()); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 5858d8ed03..4a2fe6bf2c 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -180,6 +180,11 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { if ($this->isArray()->no()) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 46a9369699..77a07d300c 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -273,6 +273,11 @@ public function unsetOffset(Type $offsetType): Type return new NeverType(); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return new NeverType(); diff --git a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php index 550e668893..bb7a7bea88 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php @@ -7,6 +7,7 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -29,15 +30,27 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (count($functionCall->getArgs()) !== 1) { + $args = $functionCall->getArgs(); + if (count($args) < 1) { return null; } - $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } + if (count($args) >= 2) { + $filterType = $scope->getType($args[1]->value); + + $strict = TrinaryLogic::createNo(); + if (count($args) >= 3) { + $strict = $scope->getType($args[2]->value)->isTrue(); + } + + return $arrayType->getKeysArrayFiltered($filterType, $strict); + } + return $arrayType->getKeysArray(); } diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index cd82cdb9ea..e9b06e82c8 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -395,6 +395,11 @@ public function unsetOffset(Type $offsetType): Type return $this->getStaticObjectType()->unsetOffset($offsetType); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getStaticObjectType()->getKeysArrayFiltered($filterValueType, $strict); + } + public function getKeysArray(): Type { return $this->getStaticObjectType()->getKeysArray(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index a909030230..11f8f3048b 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -248,6 +248,11 @@ public function unsetOffset(Type $offsetType): Type return $this->resolve()->unsetOffset($offsetType); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->resolve()->getKeysArrayFiltered($filterValueType, $strict); + } + public function getKeysArray(): Type { return $this->resolve()->getKeysArray(); diff --git a/src/Type/Traits/MaybeArrayTypeTrait.php b/src/Type/Traits/MaybeArrayTypeTrait.php index f83bce156f..654429b3c6 100644 --- a/src/Type/Traits/MaybeArrayTypeTrait.php +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -39,6 +39,11 @@ public function isList(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return new ErrorType(); diff --git a/src/Type/Traits/NonArrayTypeTrait.php b/src/Type/Traits/NonArrayTypeTrait.php index 897ffdb2ef..6ce17430db 100644 --- a/src/Type/Traits/NonArrayTypeTrait.php +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -39,6 +39,11 @@ public function isList(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->getKeysArray(); + } + public function getKeysArray(): Type { return new ErrorType(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 3882f6b5d3..7732a77639 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -134,6 +134,8 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T public function unsetOffset(Type $offsetType): Type; + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type; + public function getKeysArray(): Type; public function getValuesArray(): Type; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index d8116a737d..717c812cd9 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -731,6 +731,11 @@ public function unsetOffset(Type $offsetType): Type return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); } + public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict)); + } + public function getKeysArray(): Type { return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArray()); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11928.php b/tests/PHPStan/Analyser/nsrt/bug-11928.php new file mode 100644 index 0000000000..94317f690f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11928.php @@ -0,0 +1,68 @@ + 1, 3 => 2, 4 => 1]; + + $keys = array_keys($a, 1); // returns [2, 4] + assertType('list<2|3|4>', $keys); + + $keys = array_keys($a); // returns [2, 3, 4] + assertType('array{2, 3, 4}', $keys); +} + +/** + * @param array<1|2|3, 4|5|6> $unionKeyedArray + * @param 4|5 $fourOrFive + * @return void + */ +function doFooStrings($unionKeyedArray, $fourOrFive) { + $a = [2 => 'hi', 3 => '123', 'xy' => 5]; + $keys = array_keys($a, 1); + assertType("list<2|3|'xy'>", $keys); + + $keys = array_keys($a); + assertType("array{2, 3, 'xy'}", $keys); + + $keys = array_keys($unionKeyedArray, 1); + assertType("list<1|2|3>", $keys); // could be array{} + + $keys = array_keys($unionKeyedArray, 4); + assertType("list<1|2|3>", $keys); + + $keys = array_keys($unionKeyedArray, $fourOrFive); + assertType("list<1|2|3>", $keys); + + $keys = array_keys($unionKeyedArray); + assertType("list<1|2|3>", $keys); +} + +/** + * @param array $array + * @param list $list + * @param array $strings + * @return void + */ +function doFooBar(array $array, array $list, array $strings) { + $keys = array_keys($strings, "a", true); + assertType('list', $keys); + + $keys = array_keys($strings, "a", false); + assertType('list', $keys); + + $keys = array_keys($array, 1, true); + assertType('list', $keys); + + $keys = array_keys($array, 1, false); + assertType('list', $keys); + + $keys = array_keys($list, 1, true); + assertType('list>', $keys); + + $keys = array_keys($list, 1, true); + assertType('list>', $keys); +}