Skip to content

Commit

Permalink
array_map - understand call with multiple arrays
Browse files Browse the repository at this point in the history
ondrejmirtes committed Sep 12, 2021

Verified

This commit was signed with the committer’s verified signature. The key has expired.
ondrejmirtes Ondřej Mirtes
1 parent 9c3b765 commit 3e0ecec
Showing 8 changed files with 162 additions and 21 deletions.
17 changes: 14 additions & 3 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
}
3 changes: 2 additions & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
@@ -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;
@@ -3157,7 +3158,7 @@ private function processArgs(
}
}

if ($calleeReflection instanceof FunctionReflection) {
if (!BleedingEdgeToggle::isBleedingEdge() && $calleeReflection instanceof FunctionReflection) {
if (
$i === 0
&& $calleeReflection->getName() === 'array_map'
18 changes: 15 additions & 3 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
@@ -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()
37 changes: 23 additions & 14 deletions src/Type/Php/ArrayMapFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -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;
@@ -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));
}
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
@@ -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');
}

/**
21 changes: 21 additions & 0 deletions tests/PHPStan/Analyser/data/array_map_multiple.php
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -820,4 +820,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
Original file line number Diff line number Diff line change
@@ -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 3e0ecec

Please sign in to comment.