From 84bafe79d6b59f18dcbc0b3c8b9978d7af9bd837 Mon Sep 17 00:00:00 2001 From: Jean-Luc Herren Date: Fri, 24 Sep 2021 22:18:11 +0200 Subject: [PATCH] Make array_map() accept null as first argument --- src/Reflection/ParametersAcceptorSelector.php | 7 +++- .../ArrayMapFunctionReturnTypeExtension.php | 33 +++++++++++++++---- .../Analyser/data/array_map_multiple.php | 15 +++++++++ .../CallToFunctionParametersRuleTest.php | 8 ++--- .../Functions/data/array_map_multiple.php | 5 +++ 5 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 3b3db43e26..08afdd5700 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -12,7 +12,9 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; /** @api */ class ParametersAcceptorSelector @@ -80,7 +82,10 @@ public static function selectFromArgs( $parameters[0] = new NativeParameterReflection( $parameters[0]->getName(), $parameters[0]->isOptional(), - new CallableType($callbackParameters, new MixedType(), false), + new UnionType([ + new CallableType($callbackParameters, new MixedType(), false), + new NullType(), + ]), $parameters[0]->passedByReference(), $parameters[0]->isVariadic(), $parameters[0]->getDefaultValue() diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 543f3ac725..1a9772dd0c 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -9,9 +9,11 @@ use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; @@ -30,23 +32,35 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $valueType = new MixedType(); + $singleArrayArgument = !isset($functionCall->getArgs()[2]); $callableType = $scope->getType($functionCall->getArgs()[0]->value); + $callableIsNull = (new NullType())->isSuperTypeOf($callableType)->yes(); + if ($callableType->isCallable()->yes()) { $valueType = new NeverType(); foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) { $valueType = TypeCombinator::union($valueType, $parametersAcceptor->getReturnType()); } + } elseif ($callableIsNull) { + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) { + $arrayBuilder->setOffsetValueType( + new ConstantIntegerType($index), + $scope->getType($arg->value)->getIterableValueType() + ); + } + $valueType = $arrayBuilder->getArray(); + } else { + $valueType = new MixedType(); } - $mappedArrayType = new ArrayType( - new MixedType(), - $valueType - ); $arrayType = $scope->getType($functionCall->getArgs()[1]->value); - $constantArrays = TypeUtils::getConstantArrays($arrayType); - if (!isset($functionCall->getArgs()[2])) { + if ($singleArrayArgument) { + if ($callableIsNull) { + return $arrayType; + } + $constantArrays = TypeUtils::getConstantArrays($arrayType); if (count($constantArrays) > 0) { $arrayTypes = []; foreach ($constantArrays as $constantArray) { @@ -66,6 +80,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType->getIterableKeyType(), $valueType ), ...TypeUtils::getAccessoryTypes($arrayType)); + } else { + $mappedArrayType = new ArrayType( + new MixedType(), + $valueType + ); } } else { $mappedArrayType = TypeCombinator::intersect(new ArrayType( diff --git a/tests/PHPStan/Analyser/data/array_map_multiple.php b/tests/PHPStan/Analyser/data/array_map_multiple.php index 651ffde7f7..a384a33ac5 100644 --- a/tests/PHPStan/Analyser/data/array_map_multiple.php +++ b/tests/PHPStan/Analyser/data/array_map_multiple.php @@ -18,4 +18,19 @@ public function doFoo(int $i, string $s): void assertType('array&nonEmpty', $result); } + /** + * @param non-empty-array $array + * @param non-empty-array $other + */ + public function arrayMapNull(array $array, array $other): void + { + assertType('array()', array_map(null, [])); + assertType('array(\'foo\' => true)', array_map(null, ['foo' => true])); + assertType('array&nonEmpty', array_map(null, [1, 2, 3], [4, 5, 6])); + + assertType('array&nonEmpty', array_map(null, $array)); + assertType('array&nonEmpty', array_map(null, $array, $array)); + assertType('array&nonEmpty', array_map(null, $array, $other)); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 89544277ab..6d0dd8a99d 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -796,7 +796,7 @@ 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.', + 'Parameter #1 $callback of function array_map expects (callable(1|2, \'bar\'|\'foo\'): mixed)|null, Closure(int, int): void given.', 58, ], ]); @@ -840,11 +840,11 @@ public function testBug5356(): void $this->analyse([__DIR__ . '/data/bug-5356.php'], [ [ - 'Parameter #1 $callback of function array_map expects callable(string): mixed, Closure(array): \'a\' given.', + 'Parameter #1 $callback of function array_map expects (callable(string): mixed)|null, Closure(array): \'a\' given.', 13, ], [ - 'Parameter #1 $callback of function array_map expects callable(string): mixed, Closure(array): \'a\' given.', + 'Parameter #1 $callback of function array_map expects (callable(string): mixed)|null, Closure(array): \'a\' given.', 21, ], ]); @@ -854,7 +854,7 @@ public function testBug1954(): void { $this->analyse([__DIR__ . '/data/bug-1954.php'], [ [ - 'Parameter #1 $callback of function array_map expects callable(1|stdClass): mixed, Closure(string): string given.', + 'Parameter #1 $callback of function array_map expects (callable(1|stdClass): mixed)|null, Closure(string): string given.', 7, ], ]); diff --git a/tests/PHPStan/Rules/Functions/data/array_map_multiple.php b/tests/PHPStan/Rules/Functions/data/array_map_multiple.php index 06d872a41f..0d96ce95e9 100644 --- a/tests/PHPStan/Rules/Functions/data/array_map_multiple.php +++ b/tests/PHPStan/Rules/Functions/data/array_map_multiple.php @@ -60,4 +60,9 @@ public function doFoo(): void }, [1, 2], ['foo', 'bar']); } + public function arrayMapNull(): void + { + array_map(null, [1, 2], [3, 4]); + } + }