Skip to content

Commit

Permalink
Consider optional keys in ConstantArrayType::slice
Browse files Browse the repository at this point in the history
  • Loading branch information
herndlm authored and ondrejmirtes committed May 28, 2022
1 parent 094af3d commit b24249b
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 27 deletions.
76 changes: 64 additions & 12 deletions src/Type/Constant/ConstantArrayType.php
Expand Up @@ -45,7 +45,7 @@
use function in_array;
use function is_int;
use function is_string;
use function max;
use function min;
use function pow;
use function sort;
use function sprintf;
Expand Down Expand Up @@ -643,25 +643,77 @@ private function removeFirstElements(int $length, bool $reindex = true): self

public function slice(int $offset, ?int $limit, bool $preserveKeys = false): self
{
if (count($this->keyTypes) === 0) {
$keyTypesCount = count($this->keyTypes);
if ($keyTypesCount === 0) {
return $this;
}

$keyTypes = array_slice($this->keyTypes, $offset, $limit);
$valueTypes = array_slice($this->valueTypes, $offset, $limit);
$limit ??= $keyTypesCount;
if ($limit < 0) {
// Negative limits prevent access to the most right n elements
return $this->removeLastElements($limit * -1)
->slice($offset, null, $preserveKeys);
}

/** @var int|float $nextAutoIndex */
$nextAutoIndex = 0;
foreach ($keyTypes as $keyType) {
if (!$keyType instanceof ConstantIntegerType) {
continue;
if ($keyTypesCount + $offset <= 0) {
// A negative offset cannot reach left outside the array
$offset = 0;
}

if ($offset < 0) {
/*
* Transforms the problem with the negative offset in one with a positive offset using array reversion.
* The reason is belows handling of optional keys which works only from left to right.
*
* e.g.
* array{a: 0, b: 1, c: 2, d: 3, e: 4}
* with offset -4 and limit 2 (which would be sliced to array{b: 1, c: 2})
*
* is transformed via reversion to
*
* array{e: 4, d: 3, c: 2, b: 1, a: 0}
* with offset 2 and limit 2 (which will be sliced to array{c: 2, b: 1} and then reversed again)
*/
$offset *= -1;
$reversedLimit = min($limit, $offset);
$reversedOffset = $offset - $reversedLimit;
return $this->reverse(true)
->slice($reversedOffset, $reversedLimit, $preserveKeys)
->reverse(true);
}

if ($offset > 0) {
return $this->removeFirstElements($offset, false)
->slice(0, $limit, $preserveKeys);
}

$builder = ConstantArrayTypeBuilder::createEmpty();

$nonOptionalElementsCount = 0;
$hasOptional = false;
for ($i = 0; $nonOptionalElementsCount < $limit && $i < $keyTypesCount; $i++) {
$isOptional = $this->isOptionalKey($i);
if (!$isOptional) {
$nonOptionalElementsCount++;
} else {
$hasOptional = true;
}

/** @var int|float $nextAutoIndex */
$nextAutoIndex = max($nextAutoIndex, $keyType->getValue() + 1);
$isLastElement = $nonOptionalElementsCount >= $limit || $i + 1 >= $keyTypesCount;
if ($isLastElement && $limit < $keyTypesCount && $hasOptional) {
// If the slice is not full yet, but has at least one optional key
// the last non-optional element is going to be optional.
// Otherwise, it would not fit into the slice if previous non-optional keys are there.
$isOptional = true;
}

$builder->setOffsetValueType($this->keyTypes[$i], $this->valueTypes[$i], $isOptional);
}

$slice = new self($keyTypes, $valueTypes, (int) $nextAutoIndex, []);
$slice = $builder->getArray();
if (!$slice instanceof self) {
throw new ShouldNotHappenException();
}

return $preserveKeys ? $slice : $slice->reindex();
}
Expand Down
74 changes: 59 additions & 15 deletions tests/PHPStan/Analyser/data/array-slice.php
Expand Up @@ -23,22 +23,66 @@ public function fromMixed($arr): void
assertType('array', array_slice($arr, 1, 2));
}

/**
* @param array<int, bool> $arr1
* @param array<string, int> $arr2
* @param array{17: 'foo', b: 'bar', 19: 'baz'} $arr3
* @param array{17: 'foo', 19: 'bar', 21: 'baz'}|array{foo: 17, bar: 19, baz: 21} $arr4
*/
public function preserveTypes(array $arr1, array $arr2, array $arr3, array $arr4): void
public function normalArrays(array $arr): void
{
/** @var array<int, bool> $arr */
assertType('array<int, bool>', array_slice($arr, 1, 2));
assertType('array<int, bool>', array_slice($arr, 1, 2, true));

/** @var array<string, int> $arr */
assertType('array<string, int>', array_slice($arr, 1, 2));
assertType('array<string, int>', array_slice($arr, 1, 2, true));
}

public function constantArrays(array $arr): void
{
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
assertType('array{b: \'bar\', 0: \'baz\'}', array_slice($arr, 1, 2));
assertType('array{b: \'bar\', 19: \'baz\'}', array_slice($arr, 1, 2, true));

/** @var array{17: 'foo', 19: 'bar', 21: 'baz'}|array{foo: 17, bar: 19, baz: 21} $arr */
assertType('array{\'bar\', \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr, 1, 2));
assertType('array{19: \'bar\', 21: \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr, 1, 2, true));
}

public function constantArraysWithOptionalKeys(array $arr): void
{
assertType('array<int, bool>', array_slice($arr1, 1, 2));
assertType('array<int, bool>', array_slice($arr1, 1, 2, true));
assertType('array<string, int>', array_slice($arr2, 1, 2));
assertType('array<string, int>', array_slice($arr2, 1, 2, true));
assertType('array{b: \'bar\', 0: \'baz\'}', array_slice($arr3, 1, 2));
assertType('array{b: \'bar\', 19: \'baz\'}', array_slice($arr3, 1, 2, true));
assertType('array{\'bar\', \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr4, 1, 2));
assertType('array{19: \'bar\', 21: \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr4, 1, 2, true));
/** @var array{a?: 0, b: 1, c: 2} $arr */
assertType('array{a?: 0, b?: 1}', array_slice($arr, 0, 1));
assertType('array{a?: 0, b: 1, c: 2}', array_slice($arr, 0));
assertType('array{a?: 0, b: 1, c: 2}', array_slice($arr, -99));
assertType('array{a?: 0, b: 1, c: 2}', array_slice($arr, 0, 99));
assertType('array{a?: 0}', array_slice($arr, 0, -2));
assertType('array{}', array_slice($arr, 0, -3));
assertType('array{}', array_slice($arr, 0, -99));
assertType('array{}', array_slice($arr, -99, -99));
assertType('array{}', array_slice($arr, 99));

/** @var array{a?: 0, b?: 1, c: 2, d: 3, e: 4} $arr */
assertType('array{c?: 2, d?: 3, e?: 4}', array_slice($arr, 2, 1));
assertType('array{b?: 1, c?: 2, d: 3, e?: 4}', array_slice($arr, 1, 3));
assertType('array{e: 4}', array_slice($arr, -1));
assertType('array{d: 3}', array_slice($arr, -2, 1));

/** @var array{a: 0, b: 1, c: 2, d?: 3, e?: 4} $arr */
assertType('array{c: 2}', array_slice($arr, 2, 1));
assertType('array{c?: 2, d?: 3, e?: 4}', array_slice($arr, -1));
assertType('array{b?: 1, c?: 2, d?: 3}', array_slice($arr, -2, 1));
assertType('array{a: 0, b: 1, c?: 2, d?: 3}', array_slice($arr, 0, -1));
assertType('array{a: 0, b?: 1, c?: 2}', array_slice($arr, 0, -2));

/** @var array{a: 0, b?: 1, c: 2, d?: 3, e: 4} $arr */
assertType('array{b?: 1, c: 2, d?: 3, e?: 4}', array_slice($arr, 1, 2));
assertType('array{a: 0, b?: 1, c?: 2}', array_slice($arr, 0, 2));
assertType('array{a: 0}', array_slice($arr, 0, 1));
assertType('array{b?: 1, c?: 2}', array_slice($arr, 1, 1));
assertType('array{c?: 2, d?: 3, e?: 4}', array_slice($arr, 2, 1));
assertType('array{a: 0, b?: 1, c: 2, d?: 3}', array_slice($arr, 0, -1));
assertType('array{c?: 2, d?: 3, e: 4}', array_slice($arr, -2));

/** @var array{a: 0, b?: 1, c: 2} $arr */
assertType('array{a: 0, b?: 1}', array_slice($arr, 0, -1));
assertType('array{a: 0}', array_slice($arr, -3, 1));
}

}

0 comments on commit b24249b

Please sign in to comment.