Skip to content

Add Type::sliceArray() #3514

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 6, 2024
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
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ parameters:

-
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#"
count: 8
count: 10
path: src/Type/Constant/ConstantArrayType.php

-
Expand Down
13 changes: 13 additions & 0 deletions src/Type/Accessory/AccessoryArrayListType.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,19 @@ public function shuffleArray(): Type
return $this;
}

public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
{
if ($preserveKeys->no()) {
return $this;
}

if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) {
return $this;
}

return new MixedType();
}

public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createYes();
Expand Down
16 changes: 16 additions & 0 deletions src/Type/Accessory/HasOffsetType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectWithoutClassType;
Expand All @@ -25,6 +26,7 @@
use PHPStan\Type\Traits\TruthyBooleanTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function sprintf;
Expand Down Expand Up @@ -208,6 +210,20 @@ public function shuffleArray(): Type
return new NonEmptyArrayType();
}

public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
{
if (
$this->offsetType->isSuperTypeOf($offsetType)->yes()
&& ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes())
) {
return $preserveKeys->yes()
? TypeCombinator::intersect($this, new NonEmptyArrayType())
: new NonEmptyArrayType();
}

return new MixedType();
}

public function isIterableAtLeastOnce(): TrinaryLogic
{
return TrinaryLogic::createYes();
Expand Down
16 changes: 16 additions & 0 deletions src/Type/Accessory/HasOffsetValueType.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectWithoutClassType;
Expand All @@ -27,6 +28,7 @@
use PHPStan\Type\Traits\TruthyBooleanTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function sprintf;
Expand Down Expand Up @@ -264,6 +266,20 @@ public function shuffleArray(): Type
return new NonEmptyArrayType();
}

public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
{
if (
$this->offsetType->isSuperTypeOf($offsetType)->yes()
&& ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes())
) {
return $preserveKeys->yes()
? TypeCombinator::intersect($this, new NonEmptyArrayType())
: new NonEmptyArrayType();
}

return new MixedType();
}

public function isIterableAtLeastOnce(): TrinaryLogic
{
return TrinaryLogic::createYes();
Expand Down
12 changes: 12 additions & 0 deletions src/Type/Accessory/NonEmptyArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,18 @@ public function shuffleArray(): Type
return $this;
}

public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
{
if (
(new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()
&& ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes())
) {
return $this;
}

return new MixedType();
}

public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createYes();
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Accessory/OversizedArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ public function shuffleArray(): Type
return $this;
}

public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
{
return $this;
}

public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createYes();
Expand Down
5 changes: 5 additions & 0 deletions src/Type/ArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,11 @@ public function shuffleArray(): Type
return AccessoryArrayListType::intersectWith(new self(new IntegerType(), $this->itemType));
}

public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
{
return $this;
}

public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createMaybe()->and($this->itemType->isString());
Expand Down
175 changes: 97 additions & 78 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use PHPStan\Type\CompoundType;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\ConstantType;
use PHPStan\Type\ConstantTypeHelper;
use PHPStan\Type\ErrorType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\Generic\TemplateTypeMap;
Expand All @@ -39,6 +40,7 @@
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
Expand Down Expand Up @@ -812,7 +814,7 @@ public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type

$keyTypesCount = count($this->keyTypes);
for ($i = 0; $i < $keyTypesCount; $i += $length) {
$chunk = $this->slice($i, $length, true);
$chunk = $this->sliceArray(new ConstantIntegerType($i), new ConstantIntegerType($length), TrinaryLogic::createYes());
$builder->setOffsetValueType(null, $preserveKeys->yes() ? $chunk : $chunk->getValuesArray());
}

Expand Down Expand Up @@ -882,7 +884,7 @@ public function popArray(): Type
return $this->removeLastElements(1);
}

private function reverseConstantArray(TrinaryLogic $preserveKeys): self
public function reverseArray(TrinaryLogic $preserveKeys): Type
{
$keyTypesReversed = array_reverse($this->keyTypes, true);
$keyTypes = array_values($keyTypesReversed);
Expand All @@ -894,11 +896,6 @@ private function reverseConstantArray(TrinaryLogic $preserveKeys): self
return $preserveKeys->yes() ? $reversed : $reversed->reindex();
}

public function reverseArray(TrinaryLogic $preserveKeys): Type
{
return $this->reverseConstantArray($preserveKeys);
}

public function searchArray(Type $needleType): Type
{
$matches = [];
Expand Down Expand Up @@ -957,6 +954,85 @@ public function shuffleArray(): Type
return $generalizedArray;
}

public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
{
$keyTypesCount = count($this->keyTypes);
if ($keyTypesCount === 0) {
return $this;
}

$offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : 0;
$length = $lengthType instanceof ConstantIntegerType ? $lengthType->getValue() : $keyTypesCount;

if ($length < 0) {
// Negative lengths prevent access to the most right n elements
return $this->removeLastElements($length * -1)
->sliceArray($offsetType, new NullType(), $preserveKeys);
}

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 length 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 length 2 (which will be sliced to array{c: 2, b: 1} and then reversed again)
*/
$offset *= -1;
$reversedLength = min($length, $offset);
$reversedOffset = $offset - $reversedLength;
return $this->reverseArray(TrinaryLogic::createYes())
->sliceArray(new ConstantIntegerType($reversedOffset), new ConstantIntegerType($reversedLength), $preserveKeys)
->reverseArray(TrinaryLogic::createYes());
}

if ($offset > 0) {
return $this->removeFirstElements($offset, false)
->sliceArray(new ConstantIntegerType(0), $lengthType, $preserveKeys);
}

$builder = ConstantArrayTypeBuilder::createEmpty();

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

$isLastElement = $nonOptionalElementsCount >= $length || $i + 1 >= $keyTypesCount;
if ($isLastElement && $length < $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 = $builder->getArray();
if (!$slice instanceof self) {
throw new ShouldNotHappenException();
}

return $preserveKeys->yes() ? $slice : $slice->reindex();
}

public function isIterableAtLeastOnce(): TrinaryLogic
{
$keysCount = count($this->keyTypes);
Expand Down Expand Up @@ -1147,87 +1223,30 @@ private function removeFirstElements(int $length, bool $reindex = true): self
return $array;
}

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

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

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->reverseConstantArray(TrinaryLogic::createYes())
->slice($reversedOffset, $reversedLimit, $preserveKeys)
->reverseConstantArray(TrinaryLogic::createYes());
}

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;
}

$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 = $builder->getArray();
if (!$slice instanceof self) {
$array = $this->sliceArray(
ConstantTypeHelper::getTypeFromValue($offset),
ConstantTypeHelper::getTypeFromValue($limit),
TrinaryLogic::createFromBoolean($preserveKeys),
);
if (!$array instanceof self) {
throw new ShouldNotHappenException();
}

return $preserveKeys ? $slice : $slice->reindex();
return $array;
}

/** @deprecated Use reverseArray() instead */
public function reverse(bool $preserveKeys = false): self
{
return $this->reverseConstantArray(TrinaryLogic::createFromBoolean($preserveKeys));
$array = $this->reverseArray(TrinaryLogic::createFromBoolean($preserveKeys));
if (!$array instanceof self) {
throw new ShouldNotHappenException();
}

return $array;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,11 @@ public function shuffleArray(): Type
return $this->intersectTypes(static fn (Type $type): Type => $type->shuffleArray());
}

public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
{
return $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
}

public function getEnumCases(): array
{
$compare = [];
Expand Down
Loading
Loading