From 768e1c3f54df2f5c6a3819b15c67b2eee2a1cffa Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Fri, 6 Sep 2024 22:24:17 +0200 Subject: [PATCH 1/2] Add `Type::sliceArray()` --- phpstan-baseline.neon | 2 +- src/Type/Accessory/AccessoryArrayListType.php | 13 ++ src/Type/Accessory/HasOffsetType.php | 16 ++ src/Type/Accessory/HasOffsetValueType.php | 16 ++ src/Type/Accessory/NonEmptyArrayType.php | 12 ++ src/Type/Accessory/OversizedArrayType.php | 5 + src/Type/ArrayType.php | 5 + src/Type/Constant/ConstantArrayType.php | 175 ++++++++++-------- src/Type/IntersectionType.php | 5 + src/Type/MixedType.php | 9 + src/Type/NeverType.php | 5 + .../ArraySliceFunctionReturnTypeExtension.php | 58 ++---- src/Type/StaticType.php | 5 + src/Type/Traits/LateResolvableTypeTrait.php | 5 + src/Type/Traits/MaybeArrayTypeTrait.php | 5 + src/Type/Traits/NonArrayTypeTrait.php | 5 + src/Type/Type.php | 2 + src/Type/UnionType.php | 5 + tests/PHPStan/Analyser/nsrt/array-slice.php | 13 ++ tests/PHPStan/Analyser/nsrt/bug-10721.php | 6 +- 20 files changed, 244 insertions(+), 123 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3f40a5c43c..197663fec6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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 - diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index d32cc8882c..fb50126b59 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -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(); diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 2194a10cef..bcd5a93122 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -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; @@ -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; @@ -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(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 853645c91a..651d81560a 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -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; @@ -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; @@ -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(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 293e1f111f..084cc28d3c 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -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(); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index fa64240e89..7bb8e5213b 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -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(); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 4543e36b77..c1d163d97c 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -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()); diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 27d874deab..259952b41b 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -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; @@ -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; @@ -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()); } @@ -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); @@ -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 = []; @@ -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); @@ -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; } /** diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 22a4d811e4..bb3eaab0e7 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -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 = []; diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index f72e8f3d76..777a47b94d 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -274,6 +274,15 @@ public function shuffleArray(): Type return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed))); } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + public function isCallable(): TrinaryLogic { if ($this->subtractedType !== null) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 1523562cab..f9e2288e28 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -341,6 +341,11 @@ public function shuffleArray(): Type return new NeverType(); } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new NeverType(); + } + public function isCallable(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 156a8c1a46..96ed0cb364 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -4,19 +4,22 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; final class ArraySliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_slice'; @@ -25,49 +28,22 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $args = $functionCall->getArgs(); - if (count($args) < 1) { - return null; - } - - $valueType = $scope->getType($args[0]->value); - if (!$valueType->isArray()->yes()) { + if (count($args) < 2) { return null; } - $offsetType = isset($args[1]) ? $scope->getType($args[1]->value) : null; - $limitType = isset($args[2]) ? $scope->getType($args[2]->value) : null; - - $constantArrays = $valueType->getConstantArrays(); - if (count($constantArrays) > 0) { - $preserveKeysType = isset($args[3]) ? $scope->getType($args[3]->value) : null; - - $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : 0; - $limit = $limitType instanceof ConstantIntegerType ? $limitType->getValue() : null; - $preserveKeys = $preserveKeysType !== null && $preserveKeysType->isTrue()->yes(); - - $results = []; - foreach ($constantArrays as $constantArray) { - $results[] = $constantArray->slice($offset, $limit, $preserveKeys); - } - - return TypeCombinator::union(...$results); + $arrayType = $scope->getType($args[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - if ($valueType->isIterableAtLeastOnce()->yes()) { - $optionalOffsetsType = TypeCombinator::union($valueType, new ConstantArrayType([], [])); - - $zero = new ConstantIntegerType(0); - if ( - ($offsetType === null || $zero->isSuperTypeOf($offsetType)->yes()) - && ($limitType === null || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($limitType)->yes()) - ) { - return TypeCombinator::intersect($optionalOffsetsType, new NonEmptyArrayType()); - } + $offsetType = $scope->getType($args[1]->value); + $lengthType = isset($args[2]) ? $scope->getType($args[2]->value) : new NullType(); - return $optionalOffsetsType; - } + $preserveKeysType = isset($args[3]) ? $scope->getType($args[3]->value) : new ConstantBooleanType(false); + $preserveKeys = (new ConstantBooleanType(true))->isSuperTypeOf($preserveKeysType); - return $valueType; + return $arrayType->sliceArray($offsetType, $lengthType, $preserveKeys); } } diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index e0d2924951..0be91db587 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -460,6 +460,11 @@ public function shuffleArray(): Type return $this->getStaticObjectType()->shuffleArray(); } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->getStaticObjectType()->sliceArray($offsetType, $lengthType, $preserveKeys); + } + public function isCallable(): TrinaryLogic { return $this->getStaticObjectType()->isCallable(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index fb03a4d170..3a90652de9 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -307,6 +307,11 @@ public function shuffleArray(): Type return $this->resolve()->shuffleArray(); } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->resolve()->sliceArray($offsetType, $lengthType, $preserveKeys); + } + public function isCallable(): TrinaryLogic { return $this->resolve()->isCallable(); diff --git a/src/Type/Traits/MaybeArrayTypeTrait.php b/src/Type/Traits/MaybeArrayTypeTrait.php index a68b357722..afafc91708 100644 --- a/src/Type/Traits/MaybeArrayTypeTrait.php +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -94,4 +94,9 @@ public function shuffleArray(): Type return new ErrorType(); } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + } diff --git a/src/Type/Traits/NonArrayTypeTrait.php b/src/Type/Traits/NonArrayTypeTrait.php index d6f332d2f2..1d1b948242 100644 --- a/src/Type/Traits/NonArrayTypeTrait.php +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -94,4 +94,9 @@ public function shuffleArray(): Type return new ErrorType(); } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + } diff --git a/src/Type/Type.php b/src/Type/Type.php index 398529994e..b0ed8a7788 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -169,6 +169,8 @@ public function shiftArray(): Type; public function shuffleArray(): Type; + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type; + /** * @return list */ diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 08b8d2c186..0a3479d9a8 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -751,6 +751,11 @@ public function shuffleArray(): Type return $this->unionTypes(static fn (Type $type): Type => $type->shuffleArray()); } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); + } + public function getEnumCases(): array { return $this->pickFromTypes( diff --git a/tests/PHPStan/Analyser/nsrt/array-slice.php b/tests/PHPStan/Analyser/nsrt/array-slice.php index b28c660786..87fa61e36f 100644 --- a/tests/PHPStan/Analyser/nsrt/array-slice.php +++ b/tests/PHPStan/Analyser/nsrt/array-slice.php @@ -89,4 +89,17 @@ public function constantArraysWithOptionalKeys(array $arr): void assertType('array{a: 0}', array_slice($arr, -3, 1)); } + + public function offsets(array $arr): void + { + if (array_key_exists(1, $arr)) { + assertType('non-empty-array', array_slice($arr, 1, null, false)); + assertType('hasOffset(1)&non-empty-array', array_slice($arr, 1, null, true)); + } + if (array_key_exists(1, $arr) && $arr[1] === 'foo') { + assertType('non-empty-array', array_slice($arr, 1, null, false)); + assertType("hasOffsetValue(1, 'foo')&non-empty-array", array_slice($arr, 1, null, true)); + } + } + } diff --git a/tests/PHPStan/Analyser/nsrt/bug-10721.php b/tests/PHPStan/Analyser/nsrt/bug-10721.php index 67acc5964d..cf7ce49272 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10721.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10721.php @@ -68,10 +68,10 @@ public function listVariants(): void assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 3)); // could be non-empty-array assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 3)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 3, true)); + assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, -1, 3, true)); assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3, true)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 3, true)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 3, true)); + assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, 1, 3, true)); // could be non-empty-array + assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, 2, 3, true)); assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 3, false)); assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3, false)); From 2d50299d28611c61e3864b6f7863c5a6bdc35d29 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 30 Sep 2024 10:07:49 +0200 Subject: [PATCH 2/2] Simplify $preserveKeysType creation --- src/Type/Php/ArraySliceFunctionReturnTypeExtension.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 96ed0cb364..75f6a14b63 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -39,11 +39,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $offsetType = $scope->getType($args[1]->value); $lengthType = isset($args[2]) ? $scope->getType($args[2]->value) : new NullType(); - $preserveKeysType = isset($args[3]) ? $scope->getType($args[3]->value) : new ConstantBooleanType(false); - $preserveKeys = (new ConstantBooleanType(true))->isSuperTypeOf($preserveKeysType); - return $arrayType->sliceArray($offsetType, $lengthType, $preserveKeys); + return $arrayType->sliceArray($offsetType, $lengthType, $preserveKeysType->isTrue()); } }