diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php index 66d39525603..01ba45116e1 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php @@ -19,9 +19,19 @@ use function array_merge; use function array_shift; +use function in_array; class ArrayPointerAdjustmentReturnTypeProvider implements FunctionReturnTypeProviderInterface { + /** + * These functions are already handled by the CoreGenericFunctions stub + */ + const IGNORE_FUNCTION_IDS_FOR_FALSE_RETURN_TYPE = [ + 'reset', + 'end', + 'current', + ]; + /** * @return array */ @@ -82,7 +92,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($value_type->isEmpty()) { $value_type = Type::getFalse(); - } elseif (($function_id !== 'reset' && $function_id !== 'end') || !$definitely_has_items) { + } elseif (!$definitely_has_items || self::isFunctionAlreadyHandledByStub($function_id)) { $value_type->addType(new TFalse); $codebase = $statements_source->getCodebase(); @@ -102,4 +112,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return $value_type; } + + private static function isFunctionAlreadyHandledByStub(string $function_id): bool + { + return !in_array($function_id, self::IGNORE_FUNCTION_IDS_FOR_FALSE_RETURN_TYPE, true); + } } diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 6cc5eec57a4..adacf7e776c 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -136,7 +136,7 @@ function array_flip(array $array) * * @param TArray $array * - * @return (TArray is array ? null : TKey|null) + * @return (TArray is array ? null : (TArray is non-empty-list ? int<0,max> : (TArray is non-empty-array ? TKey : TKey|null))) * @psalm-pure * @psalm-ignore-nullable-return */ @@ -144,6 +144,46 @@ function key($array) { } +/** + * @psalm-template TKey as array-key + * @psalm-template TValue + * @psalm-template TArray as array + * + * @param TArray $array + * + * @return (TArray is array ? false : (TArray is non-empty-array ? TValue : TValue|false)) + * @psalm-pure + */ +function current($array) +{ +} + +/** + * @psalm-template TKey as array-key + * @psalm-template TValue + * @psalm-template TArray as array + * + * @param TArray $array + * + * @return (TArray is array ? false : (TArray is non-empty-array ? TValue : TValue|false)) + */ +function reset(&$array) +{ +} + +/** + * @psalm-template TKey as array-key + * @psalm-template TValue + * @psalm-template TArray as array + * + * @param TArray $array + * + * @return (TArray is array ? false : (TArray is non-empty-array ? TValue : TValue|false)) + */ +function end(&$array) +{ +} + /** * @psalm-template TKey as array-key * @psalm-template TArray as array diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index 9a3baee55d1..0b116489f53 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -1085,7 +1085,7 @@ class Foo { $a = ["one" => 1, "two" => 3]; $b = key($a);', 'assertions' => [ - '$b' => 'null|string', + '$b' => 'string', ], ], 'keyEmptyArray' => [ @@ -1100,12 +1100,90 @@ class Foo { ' [ + ' 1, "two" => 3]; + $b = current($a);', + 'assertions' => [ + '$b' => 'int', + ], + ], + 'currentEmptyArray' => [ + ' [ + '$b' => 'false', + ], + ], + 'currentNonEmptyArray' => [ + ' $arr + * @return int + */ + function foo(array $arr) { + return current($arr); + }', + ], + 'reset' => [ + ' 1, "two" => 3]; + $b = reset($a);', + 'assertions' => [ + '$b' => 'int', + ], + ], + 'resetEmptyArray' => [ + ' [ + '$b' => 'false', + ], + ], + 'resetNonEmptyArray' => [ + ' $arr + * @return int + */ + function foo(array $arr) { + return reset($arr); + }', + ], + 'end' => [ + ' 1, "two" => 3]; + $b = end($a);', + 'assertions' => [ + '$b' => 'int', + ], + ], + 'endEmptyArray' => [ + ' [ + '$b' => 'false', + ], + ], + 'endNonEmptyArray' => [ + ' $arr + * @return int + */ + function foo(array $arr) { + return end($arr); + }', + ], 'arrayKeyFirst' => [ ' */ diff --git a/tests/TypeReconciliation/EmptyTest.php b/tests/TypeReconciliation/EmptyTest.php index 873d75c5459..71ad4d69b89 100644 --- a/tests/TypeReconciliation/EmptyTest.php +++ b/tests/TypeReconciliation/EmptyTest.php @@ -259,7 +259,6 @@ function contains(array $data, array $needle): bool { while (!empty($needle)) { $key = key($needle); - if ($key === null) continue; $val = $needle[$key]; unset($needle[$key]);