Skip to content
Merged
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
21 changes: 19 additions & 2 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
use function is_int;
use function is_nan;
use function is_string;
use const PHP_INT_MAX;

/**
* @implements ExprHandler<Assign|AssignRef>
Expand Down Expand Up @@ -1932,6 +1933,12 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam
{
$implicitIndex = 0;
foreach ($arrayExpr->items as $arrayItem) {
if ($arrayItem->unpack) {
// An unpacked array contributes an unknown number of renumbered
// integer keys, so subsequent implicit indices are unpredictable
$implicitIndex = null;
}

if ($arrayItem->key !== null) {
$keyType = $scope->getType($arrayItem->key)->toArrayKey();

Expand All @@ -1940,7 +1947,7 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam
if (count($keyValues) === 1) {
$keyValue = $keyValues[0];
if (is_int($keyValue) && $keyValue >= $implicitIndex) {
$implicitIndex = $keyValue + 1;
$implicitIndex = $this->advanceImplicitIndex($keyValue);
}
} elseif (!$keyType->isInteger()->no()) {
// Key could be an integer, but we don't know which one,
Expand All @@ -1952,7 +1959,7 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam
$dimExpr = $arrayItem->key;
} elseif ($implicitIndex !== null) {
$dimExpr = new Node\Scalar\Int_($implicitIndex);
$implicitIndex++;
$implicitIndex = $this->advanceImplicitIndex($implicitIndex);
} else {
$dimExpr = new TypeExpr(new IntegerType());
}
Expand Down Expand Up @@ -1991,6 +1998,16 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam

private const ARRAY_DIM_FETCH_WRITE_DEPTH_LIMIT = 5;

/**
* Null means the next implicit key is unpredictable: PHP throws
* "Cannot add element to the array as the next element is already occupied"
* instead of wrapping the auto-index around to a float.
*/
private function advanceImplicitIndex(int $index): ?int
{
return $index === PHP_INT_MAX ? null : $index + 1;
}

/**
* @param non-empty-list<ArrayDimFetch> $dimFetchStack
* @param non-empty-list<array{Type|null, ArrayDimFetch}> $offsetTypes
Expand Down
13 changes: 13 additions & 0 deletions src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use function max;
use function sprintf;
use function var_export;
use const PHP_INT_MAX;

/**
* @implements Rule<LiteralArrayNode>
Expand Down Expand Up @@ -63,6 +64,13 @@ public function processNode(Node $node, Scope $scope): array
continue;
}

if ($item->unpack) {
// An unpacked array contributes an unknown number of renumbered
// integer keys, so the following implicit keys are unpredictable.
$autoGeneratedIndex = false;
continue;
}

$key = $item->key;
if ($key === null) {
if ($autoGeneratedIndex === false) {
Expand All @@ -72,6 +80,11 @@ public function processNode(Node $node, Scope $scope): array
if ($autoGeneratedIndex === null) {
$autoGeneratedIndex = 0;
$keyType = new ConstantIntegerType(0);
} elseif ($autoGeneratedIndex === PHP_INT_MAX) {
// PHP throws "Cannot add element to the array as the next
// element is already occupied" instead of wrapping around.
$autoGeneratedIndex = false;
continue;
} else {
$keyType = new ConstantIntegerType(++$autoGeneratedIndex);
}
Expand Down
21 changes: 18 additions & 3 deletions src/Type/Constant/OversizedArrayBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
use PHPStan\Type\Accessory\OversizedArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\IntegerType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\VerbosityLevel;
use function array_splice;
use function array_values;
use function count;
use const PHP_INT_MAX;

#[AutowiredService]
final class OversizedArrayBuilder
Expand Down Expand Up @@ -72,13 +74,16 @@ public function build(Array_ $expr, callable $getTypeCallback): Type
$isList = false;
} elseif ($itemKeyType->getValue() !== $nextAutoIndex) {
$isList = false;
$nextAutoIndex = $itemKeyType->getValue() + 1;
$nextAutoIndex = $this->advanceAutoIndex($itemKeyType->getValue());
} else {
$nextAutoIndex++;
$nextAutoIndex = $this->advanceAutoIndex($nextAutoIndex);
}
} elseif ($nextAutoIndex === null) {
$isList = false;
$itemKeyType = new IntegerType();
} else {
$itemKeyType = new ConstantIntegerType($nextAutoIndex);
$nextAutoIndex++;
$nextAutoIndex = $this->advanceAutoIndex($nextAutoIndex);
}

$generalizedKeyType = $itemKeyType->generalize(GeneralizePrecision::moreSpecific());
Expand All @@ -101,4 +106,14 @@ public function build(Array_ $expr, callable $getTypeCallback): Type
return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType(), ...$accessories);
}

/**
* Null means the next implicit key is unpredictable: PHP throws
* "Cannot add element to the array as the next element is already occupied"
* instead of wrapping the auto-index around to a float.
*/
private function advanceAutoIndex(int $index): ?int
{
return $index === PHP_INT_MAX ? null : $index + 1;
}

}
11 changes: 10 additions & 1 deletion src/Type/Php/ArrayFillFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use const PHP_INT_MAX;

#[AutowiredService]
final class ArrayFillFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
Expand Down Expand Up @@ -64,19 +65,27 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
) {
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
$nextIndex = $startIndexType->getValue();
$overflows = false;
for ($i = 0; $i < $numberType->getValue(); $i++) {
$arrayBuilder->setOffsetValueType(
new ConstantIntegerType($nextIndex),
$valueType,
);
if ($nextIndex < 0) {
$nextIndex = 0;
} elseif ($nextIndex === PHP_INT_MAX) {
// PHP throws "Cannot add element to the array as the next
// element is already occupied" instead of wrapping around.
$overflows = $i + 1 < $numberType->getValue();
break;
} else {
$nextIndex++;
}
}

return $arrayBuilder->getArray();
if (!$overflows) {
return $arrayBuilder->getArray();
}
}

$resultType = new ArrayType(new IntegerType(), $valueType);
Expand Down
45 changes: 45 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15244.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Bug15244;

use function PHPStan\Testing\assertType;

class Foo
{

public function doArrayFill(): void
{
assertType("array{9223372036854775806: 'x', 9223372036854775807: 'x'}", array_fill(9223372036854775806, 2, 'x'));
assertType("non-empty-array<int, 'x'>", array_fill(9223372036854775806, 3, 'x'));
assertType("array{9223372036854775807: 'x'}", array_fill(9223372036854775807, 1, 'x'));
assertType("non-empty-array<int, 'x'>", array_fill(9223372036854775807, 2, 'x'));
}

public function doArrayByRef(): void
{
$b = 1;
$a = [9223372036854775807 => 1, &$b];
assertType('array{9223372036854775807: 1}', $a);
}

public function doOversizedArray(): void
{
$a = [
9223372036854775806 => 'a', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
];
assertType('non-empty-array<int, literal-string&lowercase-string&non-falsy-string>&oversized-array', $a);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,18 @@ public function testBug14041(): void
$this->analyse([__DIR__ . '/data/bug-14041.php'], []);
}

public function testBug15244(): void
{
$this->analyse([__DIR__ . '/data/bug-15244.php'], [
[
'Array has 2 duplicate keys with value 9223372036854775807 (9223372036854775807, 9223372036854775807).',
38,
],
[
'Array has 2 duplicate keys with value 0 (0, 0).',
49,
],
]);
}

}
52 changes: 52 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-15244.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Bug15244;

class Foo
{

/**
* @return array<int, int>
*/
public function doFoo(): array
{
return [9223372036854775807 => 1, 2];
}

/**
* @return array<int, int>
*/
public function doBar(): array
{
return ['9223372036854775807' => 1, 2];
}

/**
* @return array<int, int>
*/
public function doBaz(): array
{
return [9223372036854775806 => 1, 2, 3];
}

/**
* @return array<int, int>
*/
public function doLorem(): array
{
// the implicit key of `2` is PHP_INT_MAX
return [9223372036854775806 => 1, 2, 9223372036854775807 => 3];
}

/**
* @param array<int, string> $b
*/
public function doUnpack(array $b): void
{
$a = ['a', ...$b, 1 => 'x'];
$c = ['a', ...[], 1 => 'x'];
$d = ['a', ...['k' => 1], 1 => 'x'];
$e = ['a', ...$b, 0 => 'x'];
}

}
Loading