Skip to content

Commit

Permalink
Add array_combine dynamicExtension
Browse files Browse the repository at this point in the history
  • Loading branch information
VincentLanglet committed Jan 9, 2021
1 parent 1f441e1 commit ac54013
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 0 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Expand Up @@ -761,6 +761,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

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

-
class: PHPStan\Type\Php\ArrayCurrentDynamicReturnTypeExtension
tags:
Expand Down
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Expand Up @@ -91,4 +91,9 @@ public function supportsNamedArguments(): bool
return $this->versionId >= 80000;
}

public function throwsTypeErrorForInternalFunctions(): bool
{
return $this->versionId >= 80000;
}

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

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PHPStan\Analyser\Scope;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;

class ArrayCombineFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
{

private PhpVersion $phpVersion;

public function __construct(PhpVersion $phpVersion)
{
$this->phpVersion = $phpVersion;
}

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

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
if (count($functionCall->args) < 2) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

$firstArg = $functionCall->args[0]->value;
$secondArg = $functionCall->args[1]->value;

$keysParamType = $scope->getType($firstArg);
$valuesParamType = $scope->getType($secondArg);

if (
$keysParamType instanceof ConstantArrayType
&& $valuesParamType instanceof ConstantArrayType
) {
$keyTypes = $keysParamType->getValueTypes();
$valueTypes = $valuesParamType->getValueTypes();

if (count($keyTypes) !== count($valueTypes)) {
return new ConstantBooleanType(false);
}

$keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes);
if ($keyTypes !== null) {
return new ConstantArrayType(
$keyTypes,
$valueTypes
);
}
}

$arrayType = new ArrayType(
$keysParamType instanceof ArrayType ? $keysParamType->getItemType() : new MixedType(),
$valuesParamType instanceof ArrayType ? $valuesParamType->getItemType() : new MixedType()
);

if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
return $arrayType;
}

if ($firstArg instanceof Variable && $secondArg instanceof Variable && $firstArg->name === $secondArg->name) {
return $arrayType;
}

return new UnionType([$arrayType, new ConstantBooleanType(false)]);
}

/**
* @param array<int, Type> $types
*
* @return array<int, ConstantIntegerType|ConstantStringType>|null
*/
private function sanitizeConstantArrayKeyTypes(array $types): ?array
{
$sanitizedTypes = [];

foreach ($types as $type) {
if (
!$type instanceof ConstantIntegerType
&& !$type instanceof ConstantStringType
) {
return null;
}

$sanitizedTypes[] = $type;
}

return $sanitizedTypes;
}

}
28 changes: 28 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -4993,8 +4993,36 @@ public function dataArrayFunctions(): array
],
[
PHP_VERSION_ID < 80000 ? 'array|false' : 'array',
'array_combine($array, $array2)',
],
[
'array(1 => 2)',
'array_combine([1], [2])',
],
[
'false',
'array_combine([1, 2], [3])',
],
[
'array(\'a\' => \'d\', \'b\' => \'e\', \'c\' => \'f\')',
'array_combine([\'a\', \'b\', \'c\'], [\'d\', \'e\', \'f\'])',
],
[
PHP_VERSION_ID < 80000 ? 'array<1|2|3, mixed>|false' : 'array<1|2|3, mixed>',
'array_combine([1, 2, 3], $array)',
],
[
PHP_VERSION_ID < 80000 ? 'array<1|2|3>|false' : 'array<1|2|3>',
'array_combine($array, [1, 2, 3])',
],
[
'array',
'array_combine($array, $array)',
],
[
'array<string, string>',
'array_combine($stringArray, $stringArray)',
],
[
'array<0|1|2, 1|2|3>',
'array_diff_assoc($integers, [])',
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/data/array-functions.php
Expand Up @@ -160,6 +160,12 @@
/** @var array $array */
$array = doFoo();

/** @var array $array2 */
$array2 = doFoo();

/** @var string[] $stringArray */
$stringArray = doFoo();

$slicedOffset = array_slice(['4' => 'foo', 1 => 'bar', 'baz' => 'qux', 0 => 'quux', 'quuz' => 'corge'], 0, null, false);
$slicedOffsetWithKeys = array_slice(['4' => 'foo', 1 => 'bar', 'baz' => 'qux', 0 => 'quux', 'quuz' => 'corge'], 0, null, true);

Expand Down

0 comments on commit ac54013

Please sign in to comment.