Skip to content

Commit

Permalink
array_map - understand call with multiple arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes authored and mglaman committed Oct 7, 2021
1 parent 398afe6 commit ef53630
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 21 deletions.
17 changes: 14 additions & 3 deletions src/Analyser/MutatingScope.php
Expand Up @@ -1517,9 +1517,20 @@ private function resolveType(Expr $node): Type
&& $argOrder === 0
&& isset($funcCall->args[1])
) {
$callableParameters = [
new DummyParameter('item', $this->getType($funcCall->args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
];
if (!isset($funcCall->args[2])) {
$callableParameters = [
new DummyParameter('item', $this->getType($funcCall->args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
];
} else {
$callableParameters = [];
foreach ($funcCall->args as $i => $funcCallArg) {
if ($i === 0) {
continue;
}

$callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null);
}
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Analyser/NodeScopeResolver.php
Expand Up @@ -50,6 +50,7 @@
use PHPStan\BetterReflection\Reflector\ClassReflector;
use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
use PHPStan\DependencyInjection\BleedingEdgeToggle;
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\File\FileHelper;
Expand Down Expand Up @@ -3157,7 +3158,7 @@ private function processArgs(
}
}

if ($calleeReflection instanceof FunctionReflection) {
if (!BleedingEdgeToggle::isBleedingEdge() && $calleeReflection instanceof FunctionReflection) {
if (
$i === 0
&& $calleeReflection->getName() === 'array_map'
Expand Down
18 changes: 15 additions & 3 deletions src/Reflection/ParametersAcceptorSelector.php
Expand Up @@ -65,12 +65,24 @@ public static function selectFromArgs(
) {
$acceptor = $parametersAcceptors[0];
$parameters = $acceptor->getParameters();
if (!isset($args[2])) {
$callbackParameters = [
new DummyParameter('item', $scope->getType($args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
];
} else {
$callbackParameters = [];
foreach ($args as $i => $arg) {
if ($i === 0) {
continue;
}

$callbackParameters[] = new DummyParameter('item', $scope->getType($arg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null);
}
}
$parameters[0] = new NativeParameterReflection(
$parameters[0]->getName(),
$parameters[0]->isOptional(),
new CallableType([
new DummyParameter('item', $scope->getType($args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
], new MixedType(), false),
new CallableType($callbackParameters, new MixedType(), false),
$parameters[0]->passedByReference(),
$parameters[0]->isVariadic(),
$parameters[0]->getDefaultValue()
Expand Down
37 changes: 23 additions & 14 deletions src/Type/Php/ArrayMapFunctionReturnTypeExtension.php
Expand Up @@ -9,6 +9,7 @@
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
Expand Down Expand Up @@ -44,23 +45,31 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
);
$arrayType = $scope->getType($functionCall->args[1]->value);
$constantArrays = TypeUtils::getConstantArrays($arrayType);
if (count($constantArrays) > 0) {
$arrayTypes = [];
foreach ($constantArrays as $constantArray) {
$returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
foreach ($constantArray->getKeyTypes() as $keyType) {
$returnedArrayBuilder->setOffsetValueType(
$keyType,
$valueType
);

if (!isset($functionCall->args[2])) {
if (count($constantArrays) > 0) {
$arrayTypes = [];
foreach ($constantArrays as $constantArray) {
$returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
foreach ($constantArray->getKeyTypes() as $keyType) {
$returnedArrayBuilder->setOffsetValueType(
$keyType,
$valueType
);
}
$arrayTypes[] = $returnedArrayBuilder->getArray();
}
$arrayTypes[] = $returnedArrayBuilder->getArray();
}

$mappedArrayType = TypeCombinator::union(...$arrayTypes);
} elseif ($arrayType->isArray()->yes()) {
$mappedArrayType = TypeCombinator::union(...$arrayTypes);
} elseif ($arrayType->isArray()->yes()) {
$mappedArrayType = TypeCombinator::intersect(new ArrayType(
$arrayType->getIterableKeyType(),
$valueType
), ...TypeUtils::getAccessoryTypes($arrayType));
}
} else {
$mappedArrayType = TypeCombinator::intersect(new ArrayType(
$arrayType->getIterableKeyType(),
new IntegerType(),
$valueType
), ...TypeUtils::getAccessoryTypes($arrayType));
}
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -502,6 +502,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1870.php');
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5562.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5615.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array_map_multiple.php');
}

/**
Expand Down
21 changes: 21 additions & 0 deletions tests/PHPStan/Analyser/data/array_map_multiple.php
@@ -0,0 +1,21 @@
<?php

namespace ArrayMapMultiple;

use function PHPStan\Testing\assertType;

class Foo
{

public function doFoo(int $i, string $s): void
{
$result = array_map(function ($a, $b) {
assertType('int', $a);
assertType('string', $b);

return rand(0, 1) ? $a : $b;
}, ['foo' => $i], ['bar' => $s]);
assertType('array<int, int|string>&nonEmpty', $result);
}

}
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php
Expand Up @@ -835,4 +835,27 @@ public function testBug5609(): void
$this->analyse([__DIR__ . '/data/bug-5609.php'], []);
}

public function dataArrayMapMultiple(): array
{
return [
[true],
[false],
];
}

/**
* @dataProvider dataArrayMapMultiple
* @param bool $checkExplicitMixed
*/
public function testArrayMapMultiple(bool $checkExplicitMixed): void
{
$this->checkExplicitMixed = $checkExplicitMixed;
$this->analyse([__DIR__ . '/data/array_map_multiple.php'], [
[
'Parameter #1 $callback of function array_map expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(int, int): void given.',
58,
],
]);
}

}
63 changes: 63 additions & 0 deletions tests/PHPStan/Rules/Functions/data/array_map_multiple.php
@@ -0,0 +1,63 @@
<?php

namespace ArrayMapMultipleCallTest;

/**
* @template TKey as array-key
* @template TValue
*/
class Collection
{
/**
* @var array<TKey, TValue>
*/
protected $items = [];

/**
* @param array<TKey, TValue> $items
* @return void
*/
public function __construct($items)
{
$this->items = $items;
}

/**
* @template TMapValue
*
* @param callable(TValue, TKey): TMapValue $callback
* @return self<TKey, TMapValue>
*/
public function map(callable $callback)
{
$keys = array_keys($this->items);

$items = array_map($callback, $this->items, $keys);

return new self(array_combine($keys, $items));
}

/**
* @return array<TKey, TValue>
*/
public function all()
{
return $this->items;
}
}

class Foo
{

public function doFoo(): void
{
array_map(function (int $a, string $b) {

}, [1, 2], ['foo', 'bar']);

array_map(function (int $a, int $b) {

}, [1, 2], ['foo', 'bar']);
}

}

0 comments on commit ef53630

Please sign in to comment.