From ef53630d90764e8fcb4b4e30f363ad2be036555e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 12 Sep 2021 16:37:14 +0200 Subject: [PATCH] array_map - understand call with multiple arrays --- src/Analyser/MutatingScope.php | 17 ++++- src/Analyser/NodeScopeResolver.php | 3 +- src/Reflection/ParametersAcceptorSelector.php | 18 +++++- .../ArrayMapFunctionReturnTypeExtension.php | 37 ++++++----- .../Analyser/NodeScopeResolverTest.php | 1 + .../Analyser/data/array_map_multiple.php | 21 +++++++ .../CallToFunctionParametersRuleTest.php | 23 +++++++ .../Functions/data/array_map_multiple.php | 63 +++++++++++++++++++ 8 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/array_map_multiple.php create mode 100644 tests/PHPStan/Rules/Functions/data/array_map_multiple.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index dcdf9c4ce74..6bf6fb1cbc3 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -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); + } + } } } } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index fcc8275ecc7..7cfbdc6eccc 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -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' diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index c3411dc660a..61faa633970 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -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() diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 3b4869eff6c..7e2634ba508 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -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)); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 5b93dd30c63..937998df7ee 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -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'); } /** diff --git a/tests/PHPStan/Analyser/data/array_map_multiple.php b/tests/PHPStan/Analyser/data/array_map_multiple.php new file mode 100644 index 00000000000..651ffde7f71 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array_map_multiple.php @@ -0,0 +1,21 @@ + $i], ['bar' => $s]); + assertType('array&nonEmpty', $result); + } + +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 257218bfbaa..419965a1ae0 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/array_map_multiple.php b/tests/PHPStan/Rules/Functions/data/array_map_multiple.php new file mode 100644 index 00000000000..06d872a41fd --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_map_multiple.php @@ -0,0 +1,63 @@ + + */ + protected $items = []; + + /** + * @param array $items + * @return void + */ + public function __construct($items) + { + $this->items = $items; + } + + /** + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return self + */ + 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 + */ + 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']); + } + +}