Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Parser\ArrowFunctionArgVisitor;
use PHPStan\Parser\ClosureArgVisitor;
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
use PHPStan\Reflection\ArrayMapCallbackParameterTypes;
use PHPStan\Reflection\Native\NativeParameterReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\PassedByReference;
Expand Down Expand Up @@ -90,17 +91,38 @@ public function resolve(
);
}

$parameters = [];
$nativeParameters = [];
$types = [];
$nativeTypes = [];
$arrayTypes = [];
$nativeArrayTypes = [];
$hasUnpackedArg = false;
foreach ($intrinsicArgs as $arg) {
$hasUnpackedArg = $hasUnpackedArg || $arg->unpack;
$result = $storage !== null ? $storage->findExpressionResult($arg->value) : null;
$type = $result !== null ? $result->getType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope);
$nativeType = $result !== null ? $result->getNativeType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope->doNotTreatPhpDocTypesAsCertain());
if ($arrayMapArgs !== null) {
$arrayTypes[] = $type;
$nativeArrayTypes[] = $nativeType;
$type = $type->getIterableValueType();
$nativeType = $nativeType->getIterableValueType();
}
$types[] = $type;
$nativeTypes[] = $nativeType;
}

// an unpacked arg is one parameter here, so its arrays cannot be told apart
if ($arrayMapArgs !== null && !$hasUnpackedArg) {
$types = ArrayMapCallbackParameterTypes::addPaddingNull($types, $arrayTypes);
$nativeTypes = ArrayMapCallbackParameterTypes::addPaddingNull($nativeTypes, $nativeArrayTypes);
}

$parameters = [];
foreach ($types as $type) {
$parameters[] = new DummyParameter('item', $type, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
}
$nativeParameters = [];
foreach ($nativeTypes as $nativeType) {
$nativeParameters[] = new DummyParameter('item', $nativeType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
}

Expand Down
70 changes: 70 additions & 0 deletions src/Reflection/ArrayMapCallbackParameterTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php declare(strict_types = 1);

namespace PHPStan\Reflection;

use PHPStan\Turbo\ReferencedByTurboExtension;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function is_int;
use function max;

/**
* With more than one array, array_map() calls the callback once per index of
* the longest array, and passes null for each array that is shorter.
*/
#[ReferencedByTurboExtension(key: 'arrayMapCallbackParameterTypes')]
final class ArrayMapCallbackParameterTypes
{

/**
* Adds null to the value type of each array that is shorter than another one
* when both have their largest size. Only when the size of every array is known.
*
* The largest sizes are compared, not the smallest, because two arrays can be
* one array: array_map($cb, $a, $a) never passes null, whatever sizes $a can have.
*
* @param list<Type> $valueTypes
* @param list<Type> $arrayTypes the array each value type comes from
* @return list<Type>
*/
public static function addPaddingNull(array $valueTypes, array $arrayTypes): array
{
if (count($arrayTypes) < 2) {
return $valueTypes;
}

$maxSizes = [];
foreach ($arrayTypes as $i => $arrayType) {
$sizes = [];
foreach ($arrayType->getArraySize()->getFiniteTypes() as $sizeType) {
foreach ($sizeType->getConstantScalarValues() as $size) {
if (!is_int($size)) {
continue;
}

$sizes[] = $size;
}
}

if ($sizes === []) {
// general arrays can have the same size, but the type system cannot say so
return $valueTypes;
}

$maxSizes[$i] = max($sizes);
}

$longestSize = max($maxSizes);
foreach ($valueTypes as $i => $valueType) {
if ($maxSizes[$i] === $longestSize) {
continue;
}

$valueTypes[$i] = TypeCombinator::addNull($valueType);
}

return $valueTypes;
}

}
27 changes: 21 additions & 6 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,10 @@ public static function applyIntrinsicArgOverrides(
) {
$arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME);
if ($arrayMapArgs !== null) {
$callbackParameters = [];
$nativeCallbackParameters = [];
$callbackValueTypes = [];
$callbackArrayTypes = [];
$nativeCallbackValueTypes = [];
$nativeCallbackArrayTypes = [];
foreach ($arrayMapArgs as $arg) {
$argType = ($typeGetter)($arg->value);
$nativeArgType = ($nativeTypeGetter)($arg->value);
Expand All @@ -217,7 +219,8 @@ public static function applyIntrinsicArgOverrides(
foreach ($constantArrays as $constantArray) {
$valueTypes = $constantArray->getValueTypes();
foreach ($valueTypes as $valueType) {
$callbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
$callbackValueTypes[] = ($iterableValueTypeGetter)($valueType);
$callbackArrayTypes[] = $valueType;
}
}
}
Expand All @@ -226,16 +229,28 @@ public static function applyIntrinsicArgOverrides(
foreach ($nativeConstantArrays as $constantArray) {
$valueTypes = $constantArray->getValueTypes();
foreach ($valueTypes as $valueType) {
$nativeCallbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
$nativeCallbackValueTypes[] = ($iterableValueTypeGetter)($valueType);
$nativeCallbackArrayTypes[] = $valueType;
}
}
}
} else {
$callbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
$nativeCallbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
$callbackValueTypes[] = ($iterableValueTypeGetter)($argType);
$callbackArrayTypes[] = $argType;
$nativeCallbackValueTypes[] = ($iterableValueTypeGetter)($nativeArgType);
$nativeCallbackArrayTypes[] = $nativeArgType;
}
}

$callbackParameters = [];
foreach (ArrayMapCallbackParameterTypes::addPaddingNull($callbackValueTypes, $callbackArrayTypes) as $valueType) {
$callbackParameters[] = new DummyParameter('item', $valueType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
}
$nativeCallbackParameters = [];
foreach (ArrayMapCallbackParameterTypes::addPaddingNull($nativeCallbackValueTypes, $nativeCallbackArrayTypes) as $valueType) {
$nativeCallbackParameters[] = new DummyParameter('item', $valueType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
}

$acceptor = $parametersAcceptors[0];
$parameters = $acceptor->getParameters();
if (isset($parameters[0])) {
Expand Down
12 changes: 10 additions & 2 deletions src/Type/Php/ArrayMapFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Parser\ArrayMapArgVisitor;
use PHPStan\Reflection\ArrayMapCallbackParameterTypes;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryArrayListType;
Expand All @@ -25,6 +26,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use function array_filter;
use function array_map;
use function array_reduce;
use function array_slice;
Expand Down Expand Up @@ -53,11 +55,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
$callableIsNull = $callableType->isNull()->yes();

if ($callableType->isCallable()->yes()) {
$arrayArgs = array_slice($args, 1);
$arrayTypes = array_map(static fn (Node\Arg $arg) => $scope->getType($arg->value), $arrayArgs);
$callbackValueTypes = array_map(static fn (Type $arrayType) => $arrayType->getIterableValueType(), $arrayTypes);
if (count(array_filter($arrayArgs, static fn (Node\Arg $arg) => $arg->unpack)) === 0) {
$callbackValueTypes = ArrayMapCallbackParameterTypes::addPaddingNull($callbackValueTypes, $arrayTypes);
}
$valueType = $scope->getType(new FuncCall(
$callback,
array_map(
static fn (Node\Arg $arg) => new Node\Arg(new TypeExpr($scope->getType($arg->value)->getIterableValueType())),
array_slice($args, 1),
static fn (Type $valueType) => new Node\Arg(new TypeExpr($valueType)),
$callbackValueTypes,
),
));
} elseif ($callableIsNull) {
Expand Down
15 changes: 12 additions & 3 deletions src/Type/Php/ArrayMapParameterClosureTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\ArrayMapCallbackParameterTypes;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\Native\NativeParameterReflection;
use PHPStan\Reflection\ParameterReflection;
Expand All @@ -31,7 +32,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
return null;
}

$callbackParameters = [];
$valueTypes = [];
$arrayTypes = [];
$argCount = count($args);
for ($i = 1; $i < $argCount; $i++) {
$arg = $args[$i];
Expand All @@ -43,14 +45,21 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
}
foreach ($constantArrays as $constantArray) {
foreach ($constantArray->getValueTypes() as $valueType) {
$callbackParameters[] = $this->createParameter($scope->getIterableValueType($valueType));
$valueTypes[] = $scope->getIterableValueType($valueType);
$arrayTypes[] = $valueType;
}
}
} else {
$callbackParameters[] = $this->createParameter($scope->getIterableValueType($arrayType));
$valueTypes[] = $scope->getIterableValueType($arrayType);
$arrayTypes[] = $arrayType;
}
}

$callbackParameters = [];
foreach (ArrayMapCallbackParameterTypes::addPaddingNull($valueTypes, $arrayTypes) as $valueType) {
$callbackParameters[] = $this->createParameter($valueType);
}

return new ClosureType($callbackParameters, new MixedType());
}

Expand Down
112 changes: 112 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13217.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace Bug13217;

use function PHPStan\Testing\assertType;

class Foo
{

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

array_map(fn ($a, $b) => assertType('3|null', $b), [1, 2], [3]);

assertType('non-empty-list<1|3>', array_map(fn ($a, $b) => $b ?? 1, [1, 2], [3]));
}

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

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

/**
* @param array{1, 2}|array{1} $maybe
*/
public function sizeRange(array $maybe): void
{
array_map(function ($a, $b) {
assertType('1|2|null', $a);
assertType('3|4|5', $b);
}, $maybe, [3, 4, 5]);

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

array_map(function ($a, $b) {
assertType('1|2', $a);
assertType('1|2', $b);
}, $maybe, $maybe);
}

public function unpacked(): void
{
// the closure gets one parameter per arg here, not one per unpacked array
array_map(function ($a, $b) {
assertType('1|2|3', $a);
assertType('array{4, 5, 6}', $b);
}, [1, 2, 3], ...[[4, 5, 6]]);
}

public function singleArray(): void
{
array_map(function ($a) {
assertType('1|2', $a);
}, [1, 2]);
}

/**
* @param list<int> $ints
* @param list<string> $strings
*/
public function generalArrays(array $ints, array $strings): void
{
array_map(function ($a, $b) {
assertType('int', $a);
assertType('string', $b);
}, $ints, $strings);

array_map(function ($a) {
assertType('int', $a);
}, $ints);

array_map(function ($a, $b) {
assertType('int', $a);
assertType('3', $b);
}, $ints, [3]);
}

public function returnType(): void
{
assertType('non-empty-list<3|null>', array_map('Bug13217\\pick', [1, 2], [3]));
}

}

/**
* @template T
* @param mixed $a
* @param T $b
* @return T
*/
function pick($a, $b)
{
return $b;
}
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,11 @@ public function testInTrait(): void
]);
}

public function testBug13217(): void
{
$this->analyse([__DIR__ . '/data/bug-13217.php'], []);
}

public function testBug12780(): void
{
$this->analyse([__DIR__ . '/data/bug-12780.php'], []);
Expand Down
3 changes: 3 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-13217.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php declare(strict_types = 1);

array_map(fn($_, $bValue) => $bValue ?? 1, [1, 2], [3]);
Loading
Loading