Skip to content

Commit

Permalink
Merge pull request #8584 from boesing/feature/8521
Browse files Browse the repository at this point in the history
Enhance type detection for internal php functions `key`, `current`, `end` and `reset`
  • Loading branch information
orklah committed Oct 17, 2022
2 parents 4025ce7 + eb6bbfb commit 48b8efd
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 5 deletions.
Expand Up @@ -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<lowercase-string>
*/
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
}
42 changes: 41 additions & 1 deletion stubs/CoreGenericFunctions.phpstub
Expand Up @@ -136,14 +136,54 @@ function array_flip(array $array)
*
* @param TArray $array
*
* @return (TArray is array<empty, empty> ? null : TKey|null)
* @return (TArray is array<empty,empty> ? null : (TArray is non-empty-list ? int<0,max> : (TArray is non-empty-array ? TKey : TKey|null)))
* @psalm-pure
* @psalm-ignore-nullable-return
*/
function key($array)
{
}

/**
* @psalm-template TKey as array-key
* @psalm-template TValue
* @psalm-template TArray as array<TKey, TValue>
*
* @param TArray $array
*
* @return (TArray is array<empty,empty> ? 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<TKey, TValue>
*
* @param TArray $array
*
* @return (TArray is array<empty,empty> ? 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<TKey, TValue>
*
* @param TArray $array
*
* @return (TArray is array<empty,empty> ? false : (TArray is non-empty-array ? TValue : TValue|false))
*/
function end(&$array)
{
}

/**
* @psalm-template TKey as array-key
* @psalm-template TArray as array<TKey, mixed>
Expand Down
82 changes: 80 additions & 2 deletions tests/ArrayFunctionCallTest.php
Expand Up @@ -1085,7 +1085,7 @@ class Foo {
$a = ["one" => 1, "two" => 3];
$b = key($a);',
'assertions' => [
'$b' => 'null|string',
'$b' => 'string',
],
],
'keyEmptyArray' => [
Expand All @@ -1100,12 +1100,90 @@ class Foo {
'<?php
/**
* @param non-empty-array $arr
* @return null|array-key
* @return array-key
*/
function foo(array $arr) {
return key($arr);
}',
],
'current' => [
'<?php
$a = ["one" => 1, "two" => 3];
$b = current($a);',
'assertions' => [
'$b' => 'int',
],
],
'currentEmptyArray' => [
'<?php
$a = [];
$b = current($a);',
'assertions' => [
'$b' => 'false',
],
],
'currentNonEmptyArray' => [
'<?php
/**
* @param non-empty-array<int> $arr
* @return int
*/
function foo(array $arr) {
return current($arr);
}',
],
'reset' => [
'<?php
$a = ["one" => 1, "two" => 3];
$b = reset($a);',
'assertions' => [
'$b' => 'int',
],
],
'resetEmptyArray' => [
'<?php
$a = [];
$b = reset($a);',
'assertions' => [
'$b' => 'false',
],
],
'resetNonEmptyArray' => [
'<?php
/**
* @param non-empty-array<int> $arr
* @return int
*/
function foo(array $arr) {
return reset($arr);
}',
],
'end' => [
'<?php
$a = ["one" => 1, "two" => 3];
$b = end($a);',
'assertions' => [
'$b' => 'int',
],
],
'endEmptyArray' => [
'<?php
$a = [];
$b = end($a);',
'assertions' => [
'$b' => 'false',
],
],
'endNonEmptyArray' => [
'<?php
/**
* @param non-empty-array<int> $arr
* @return int
*/
function foo(array $arr) {
return end($arr);
}',
],
'arrayKeyFirst' => [
'<?php
/** @return array<string, int> */
Expand Down
1 change: 0 additions & 1 deletion tests/TypeReconciliation/EmptyTest.php
Expand Up @@ -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]);
Expand Down

0 comments on commit 48b8efd

Please sign in to comment.