From 52a35d72ee053d66133f1aeb09694afa2c4e59ec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 14:14:18 +0000 Subject: [PATCH] Fix array_map callback parameter types when arrays have different constant sizes - When array_map is called with multiple arrays of different known sizes, PHP pads shorter arrays with null, so callback parameters should be nullable - Fixed ParametersAcceptorSelector to add null to callback parameter types when constant array sizes provably differ - Fixed MutatingScope::getClosureType to apply the same logic for arrow functions and closure return type inference - Added regression test in tests/PHPStan/Analyser/nsrt/bug-13217.php Closes https://github.com/phpstan/phpstan/issues/13217 --- src/Analyser/MutatingScope.php | 22 +++++++- src/Reflection/ParametersAcceptorSelector.php | 31 ++++++++++- tests/PHPStan/Analyser/nsrt/bug-13217.php | 54 +++++++++++++++++++ 3 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13217.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index a26a86a066..d954f47dbb 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -5706,8 +5706,28 @@ private function getClosureType(Expr\Closure|Expr\ArrowFunction $node): ClosureT $immediatelyInvokedArgs = $node->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME); if ($arrayMapArgs !== null) { $callableParameters = []; + $addNull = false; + if (count($arrayMapArgs) > 1) { + $expectedSize = null; + foreach ($arrayMapArgs as $funcCallArg) { + $argType = $this->getType($funcCallArg->value); + $arraySizes = $argType->getArraySize()->getConstantScalarValues(); + if (count($arraySizes) !== 1) { + break; + } + $expectedSize ??= $arraySizes[0]; + if ($expectedSize !== $arraySizes[0]) { + $addNull = true; + break; + } + } + } foreach ($arrayMapArgs as $funcCallArg) { - $callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $type = $this->getType($funcCallArg->value)->getIterableValueType(); + if ($addNull) { + $type = TypeCombinator::addNull($type); + } + $callableParameters[] = new DummyParameter('item', $type, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } } elseif ($immediatelyInvokedArgs !== null) { foreach ($immediatelyInvokedArgs as $immediatelyInvokedArg) { diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 20317a1405..aa130606de 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -86,6 +86,25 @@ public static function selectFromArgs( ) { $arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); if ($arrayMapArgs !== null) { + $addNull = false; + if (count($arrayMapArgs) > 1) { + $allSizesKnown = true; + $expectedSize = null; + foreach ($arrayMapArgs as $arg) { + $argType = $scope->getType($arg->value); + $arraySizes = $argType->getArraySize()->getConstantScalarValues(); + if (count($arraySizes) !== 1) { + $allSizesKnown = false; + break; + } + $expectedSize ??= $arraySizes[0]; + if ($expectedSize !== $arraySizes[0]) { + $addNull = true; + break; + } + } + } + $callbackParameters = []; foreach ($arrayMapArgs as $arg) { $argType = $scope->getType($arg->value); @@ -95,12 +114,20 @@ public static function selectFromArgs( foreach ($constantArrays as $constantArray) { $valueTypes = $constantArray->getValueTypes(); foreach ($valueTypes as $valueType) { - $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $iterableValueType = $scope->getIterableValueType($valueType); + if ($addNull) { + $iterableValueType = TypeCombinator::addNull($iterableValueType); + } + $callbackParameters[] = new DummyParameter('item', $iterableValueType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } } } } else { - $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $iterableValueType = $scope->getIterableValueType($argType); + if ($addNull) { + $iterableValueType = TypeCombinator::addNull($iterableValueType); + } + $callbackParameters[] = new DummyParameter('item', $iterableValueType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13217.php b/tests/PHPStan/Analyser/nsrt/bug-13217.php new file mode 100644 index 0000000000..d1faa0aa72 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13217.php @@ -0,0 +1,54 @@ + $bValue ?? 1, [1, 2], [3]); + assertType('non-empty-list<1|3>', $result); +} + +function sameArrayVariable(array $a): void +{ + array_map(function ($x, $y) { + assertType('mixed', $x); + assertType('mixed', $y); + }, $a, $a); +} + +function singleArray(): void +{ + array_map(function ($a) { + assertType('1|2|3', $a); + }, [1, 2, 3]); +}