Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
31 changes: 29 additions & 2 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}

Expand Down
54 changes: 54 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13217.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace Bug13217;

use function PHPStan\Testing\assertType;

// When array_map is called with multiple arrays of different constant lengths,
// PHP pads the shorter arrays with null values.
// So callback parameters should be nullable when arrays have known different sizes.

function differentLengths(): void
{
array_map(function ($a, $b) {
assertType('1|2|null', $a);
assertType('3|null', $b);
}, [1, 2], [3]);
}

function sameLengths(): void
{
array_map(function ($a, $b) {
assertType('1|2', $a);
assertType('3|4', $b);
}, [1, 2], [3, 4]);
}

function unknownLengths(array $a, array $b): void
{
array_map(function ($a, $b) {
assertType('mixed', $a);
assertType('mixed', $b);
}, $a, $b);
}

function arrowFunctionDifferentLengths(): void
{
$result = array_map(fn ($_, $bValue) => $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]);
}
Loading