Skip to content

Commit

Permalink
Support for array_key_first/array_key_last
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Dec 4, 2018
1 parent d98c7ca commit f0252a5
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 1 deletion.
10 changes: 10 additions & 0 deletions conf/config.neon
Expand Up @@ -269,6 +269,16 @@ services:
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension

-
class: PHPStan\Type\Php\ArrayKeyFirstDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArrayKeyLastDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArrayKeysFunctionDynamicReturnTypeExtension
tags:
Expand Down
2 changes: 2 additions & 0 deletions src/Reflection/SignatureMap/functionMap.php
Expand Up @@ -280,6 +280,8 @@
'array_intersect_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable', '...rest'=>'array|callable'],
'array_intersect_ukey' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'key_compare_func'=>'callable'],
'array_intersect_ukey\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable', '...rest'=>'array|callable'],
'array_key_first' => ['int|string|null', 'array' => 'array'],
'array_key_last' => ['int|string|null', 'array' => 'array'],
'array_key_exists' => ['bool', 'key'=>'string|int', 'search'=>'array'],
'array_keys' => ['array', 'input'=>'array', 'search_value='=>'mixed', 'strict='=>'bool'],
'array_map' => ['array', 'callback'=>'?callable', 'input1'=>'array', '...args='=>'array'],
Expand Down
10 changes: 10 additions & 0 deletions src/Type/ArrayType.php
Expand Up @@ -268,6 +268,16 @@ public function getLastValueType(): Type
return $this->getFirstValueType();
}

public function getFirstKeyType(): Type
{
return TypeCombinator::union($this->getIterableKeyType(), new NullType());
}

public function getLastKeyType(): Type
{
return $this->getFirstKeyType();
}

public static function castToArrayKeyType(Type $offsetType): Type
{
if ($offsetType instanceof UnionType) {
Expand Down
19 changes: 19 additions & 0 deletions src/Type/Constant/ConstantArrayType.php
Expand Up @@ -365,6 +365,25 @@ public function getLastValueType(): Type
return $this->valueTypes[$length - 1];
}

public function getFirstKeyType(): Type
{
if (count($this->keyTypes) === 0) {
return new NullType();
}

return $this->keyTypes[0];
}

public function getLastKeyType(): Type
{
$length = count($this->keyTypes);
if ($length === 0) {
return new NullType();
}

return $this->keyTypes[$length - 1];
}

public function toBoolean(): BooleanType
{
return new ConstantBooleanType(count($this->keyTypes) > 0);
Expand Down
41 changes: 41 additions & 0 deletions src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php
@@ -0,0 +1,41 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;

class ArrayKeyFirstDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'array_key_first';
}

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
if (!isset($functionCall->args[0])) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

$argType = $scope->getType($functionCall->args[0]->value);
$arrayTypes = TypeUtils::getArrays($argType);
if (count($arrayTypes) > 0) {
$resultTypes = [];
foreach ($arrayTypes as $arrayType) {
$resultTypes[] = $arrayType->getFirstKeyType();
}

return TypeCombinator::union(...$resultTypes);
}

return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

}
41 changes: 41 additions & 0 deletions src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php
@@ -0,0 +1,41 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;

class ArrayKeyLastDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'array_key_last';
}

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
if (!isset($functionCall->args[0])) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

$argType = $scope->getType($functionCall->args[0]->value);
$arrayTypes = TypeUtils::getArrays($argType);
if (count($arrayTypes) > 0) {
$resultTypes = [];
foreach ($arrayTypes as $arrayType) {
$resultTypes[] = $arrayType->getLastKeyType();
}

return TypeCombinator::union(...$resultTypes);
}

return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

}
48 changes: 48 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -7674,6 +7674,54 @@ public function dataPhp73Functions(): array
'mixed', // will be difference type (mixed minus false) in the future
'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)',
],
[
'int|string|null',
'array_key_first($mixedArray)',
],
[
'int|string|null',
'array_key_last($mixedArray)',
],
/*[
'int|string',
'array_key_first($nonEmptyArray)',
],
[
'int|string',
'array_key_last($nonEmptyArray)',
],*/
[
'string|null',
'array_key_first($arrayWithStringKeys)',
],
[
'string|null',
'array_key_last($arrayWithStringKeys)',
],
[
'null',
'array_key_first($emptyArray)',
],
[
'null',
'array_key_last($emptyArray)',
],
[
'0',
'array_key_first($literalArray)',
],
[
'2',
'array_key_last($literalArray)',
],
[
'0',
'array_key_first($anotherLiteralArray)',
],
[
'2|3',
'array_key_last($anotherLiteralArray)',
],
];
}

Expand Down
22 changes: 21 additions & 1 deletion tests/PHPStan/Analyser/data/php73_functions.php
Expand Up @@ -5,10 +5,30 @@
class Foo
{

/**
* @param $mixed
* @param array $mixedArray
* @param array $nonEmptyArray
* @param array<string, mixed> $arrayWithStringKeys
*/
public function doFoo(
$mixed
$mixed,
array $mixedArray,
array $nonEmptyArray,
array $arrayWithStringKeys
)
{
if (count($nonEmptyArray) === 0) {
return;
}

$emptyArray = [];
$literalArray = [1, 2, 3];
$anotherLiteralArray = $literalArray;
if (rand(0, 1) === 0) {
$anotherLiteralArray[] = 4;
}

die;
}

Expand Down

0 comments on commit f0252a5

Please sign in to comment.