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 committed May 24, 2022
1 parent 34ad812 commit 286f1b4
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 26 deletions.
43 changes: 32 additions & 11 deletions src/Type/Constant/ConstantArrayType.php
Expand Up @@ -595,25 +595,46 @@ public function removeFirst(): Type

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);
$optionalKeysCount = count($this->optionalKeys);
if ($offset < 0) {
$offset = max(0, $keyTypesCount + $offset - $optionalKeysCount);
}
if ($limit !== null && $limit < 0) {
$limit = max(0, $keyTypesCount + $limit - $offset - $optionalKeysCount);
}
if ($limit === 0) {
return new self([], []);
}

/** @var int|float $nextAutoIndex */
$nextAutoIndex = 0;
foreach ($keyTypes as $keyType) {
if (!$keyType instanceof ConstantIntegerType) {
continue;
$builder = ConstantArrayTypeBuilder::createEmpty();
$sliceIndex = 0;
foreach ($this->keyTypes as $i => $keyType) {
$isOptional = $this->isOptionalKey($i);
$nextSliceIndex = $isOptional ? $sliceIndex : $sliceIndex + 1;
$reachedLimit = $limit !== null && $nextSliceIndex - $offset >= $limit;

if ($sliceIndex >= $offset) {
// The last value gets optional if the limit was reached and optional values have been added before
$currentOffsetIsOptional = $isOptional || ($reachedLimit && $i > $sliceIndex);
$builder->setOffsetValueType($keyType, $this->valueTypes[$i], $currentOffsetIsOptional);
}

/** @var int|float $nextAutoIndex */
$nextAutoIndex = max($nextAutoIndex, $keyType->getValue() + 1);
$sliceIndex = $nextSliceIndex;

if ($reachedLimit) {
break;
}
}

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

return $preserveKeys ? $slice : $slice->reindex();
}
Expand Down
54 changes: 39 additions & 15 deletions tests/PHPStan/Analyser/data/array-slice.php
Expand Up @@ -23,22 +23,46 @@ 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 preserveTypes(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<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));

/** @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));
assertType('array{0: \'foo\', b: \'bar\'}', array_slice($arr, 0, -1));
assertType('array{17: \'foo\', b: \'bar\'}', array_slice($arr, 0, -1, true));
assertType('array{b: \'bar\', 0: \'baz\'}', array_slice($arr, -2, null));
assertType('array{b: \'bar\', 19: \'baz\'}', array_slice($arr, -2, null, 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));

/** @var array{foo?: 17, bar: 19} $arr */
assertType('array{}', array_slice($arr, 0, 0));
assertType('array{}', array_slice($arr, -1, 0));
assertType('array{}', array_slice($arr, 1, 0));
assertType('array{}', array_slice($arr, 1, 1));
assertType('array{foo?: 17, bar?: 19}', array_slice($arr, 0, 1));
assertType('array{foo?: 17, bar: 19}', array_slice($arr, 0));
assertType('array{foo?: 17, bar: 19}', array_slice($arr, -99));
assertType('array{foo?: 17, bar: 19}', array_slice($arr, 0, 99));

/** @var array{foo: 17, bar?: 19, baz: 21, foobar?: 23, barfoo: 25} $arr */
assertType('array{bar?: 19, baz: 21, foobar?: 23, barfoo?: 25}', array_slice($arr, 1, 2));
assertType('array{foo: 17, bar?: 19, baz?: 21}', array_slice($arr, 0, 2));
assertType('array{foo: 17}', array_slice($arr, 0, 1));
assertType('array{bar?: 19, baz?: 21}', array_slice($arr, 1, 1));
assertType('array{foobar?: 23, barfoo?: 25}', array_slice($arr, 2, 1));
assertType('array{foo: 17, bar?: 19, baz?: 21}', array_slice($arr, 0, -1));
assertType('array{bar?: 19, baz: 21, foobar?: 23, barfoo: 25}', array_slice($arr, -2, null));
}

}

0 comments on commit 286f1b4

Please sign in to comment.