From 7f35bff0d94feab632dbe8619a10c6b2d86bdb08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Fri, 14 Oct 2022 00:58:12 +0200 Subject: [PATCH 1/2] feature: enhance type detection for internal php functions `key`, `current`, `end` and `reset` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- ...rayPointerAdjustmentReturnTypeProvider.php | 17 +++- stubs/CoreGenericFunctions.phpstub | 42 +++++++++- tests/ArrayFunctionCallTest.php | 82 ++++++++++++++++++- 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php index 66d395256..01ba45116 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php @@ -19,9 +19,19 @@ use UnexpectedValueException; 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 @@ class ArrayPointerAdjustmentReturnTypeProvider implements FunctionReturnTypeProv 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 @@ class ArrayPointerAdjustmentReturnTypeProvider implements FunctionReturnTypeProv 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 6cc5eec57..adacf7e77 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 9a3baee55..0b116489f 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -1085,7 +1085,7 @@ class ArrayFunctionCallTest extends TestCase $a = ["one" => 1, "two" => 3]; $b = key($a);', 'assertions' => [ - '$b' => 'null|string', + '$b' => 'string', ], ], 'keyEmptyArray' => [ @@ -1100,12 +1100,90 @@ class ArrayFunctionCallTest extends TestCase ' [ + ' 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' => [ ' */ From eb6bbfb3b55ab6b9bbc653060f0d169a5146a17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 17 Oct 2022 20:38:34 +0200 Subject: [PATCH 2/2] qa: remove redundant check as `!empty` already ensures that `key` does not return `null` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- tests/TypeReconciliation/EmptyTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/TypeReconciliation/EmptyTest.php b/tests/TypeReconciliation/EmptyTest.php index 873d75c54..71ad4d69b 100644 --- a/tests/TypeReconciliation/EmptyTest.php +++ b/tests/TypeReconciliation/EmptyTest.php @@ -259,7 +259,6 @@ class EmptyTest extends TestCase while (!empty($needle)) { $key = key($needle); - if ($key === null) continue; $val = $needle[$key]; unset($needle[$key]);