diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 5f60fe8eb7..8b08e67bb3 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 455f0de86e..ae42e8e00f 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -343,6 +343,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 ec6e822a31..a6e98cd24a 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 d4726fd5c2..d8d18eda84 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 c43c86a903..767389ec9e 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 68224a0f9b..ab5354d0d4 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 76fdf42a5d..1f7d7cbbdf 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; @@ -1250,6 +1251,19 @@ public function generalizeValues(): self return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } + 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 ab9f86d034..e0ccb40519 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -799,6 +799,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 487a474827..8c13827bcb 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -179,6 +179,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 518ffa8f4a..56bd9713ff 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 74a2903ea5..725d5e8b01 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -27,15 +28,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 9db20e5b34..d53b8d3530 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -396,6 +396,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 5eb703077f..1f66c36240 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 afafc91708..05e61028fe 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 1d1b948242..e048d1a148 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 15886a053c..c9827f310c 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 08d678152a..6423b05a5c 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -730,6 +730,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); +}