diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 874b177716..f80ada4ad9 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -218,6 +218,15 @@ public function popArray(): Type return $this; } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + if ($preserveKeys->no()) { + return $this; + } + + return new MixedType(); + } + public function searchArray(Type $needleType): Type { return new MixedType(); diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index cdb258d613..ac4b6e402b 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -189,6 +189,15 @@ public function intersectKeyArray(Type $otherArraysType): Type return new MixedType(); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + if ($preserveKeys->yes()) { + return $this; + } + + return new NonEmptyArrayType(); + } + public function shuffleArray(): Type { return new NonEmptyArrayType(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index a87f7879ab..59a2a26b21 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -233,6 +233,15 @@ public function intersectKeyArray(Type $otherArraysType): Type return new MixedType(); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + if ($preserveKeys->yes()) { + return $this; + } + + return new NonEmptyArrayType(); + } + public function searchArray(Type $needleType): Type { if ( diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 8ef7ecbb88..8f2ccac689 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -199,6 +199,11 @@ public function popArray(): Type return new MixedType(); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this; + } + public function searchArray(Type $needleType): Type { return new MixedType(); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index c6c9c5b6d7..ad8a1c2a13 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -195,6 +195,11 @@ public function popArray(): Type return $this; } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this; + } + public function searchArray(Type $needleType): Type { return new MixedType(); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 6040f0ee06..061e002a46 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -552,6 +552,11 @@ public function popArray(): Type return $this; } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this; + } + public function searchArray(Type $needleType): Type { return TypeCombinator::union($this->getIterableKeyType(), new ConstantBooleanType(false)); diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 3853794348..5ab51870a8 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -837,6 +837,23 @@ public function popArray(): Type return $this->removeLastElements(1); } + private function reverseConstantArray(TrinaryLogic $preserveKeys): self + { + $keyTypesReversed = array_reverse($this->keyTypes, true); + $keyTypes = array_values($keyTypesReversed); + $keyTypesReversedKeys = array_keys($keyTypesReversed); + $optionalKeys = array_map(static fn (int $optionalKey): int => $keyTypesReversedKeys[$optionalKey], $this->optionalKeys); + + $reversed = new self($keyTypes, array_reverse($this->valueTypes), $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo()); + + return $preserveKeys->yes() ? $reversed : $reversed->reindex(); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->reverseConstantArray($preserveKeys); + } + public function searchArray(Type $needleType): Type { $matches = []; @@ -1121,9 +1138,9 @@ public function slice(int $offset, ?int $limit, bool $preserveKeys = false): sel $offset *= -1; $reversedLimit = min($limit, $offset); $reversedOffset = $offset - $reversedLimit; - return $this->reverse(true) + return $this->reverseConstantArray(TrinaryLogic::createYes()) ->slice($reversedOffset, $reversedLimit, $preserveKeys) - ->reverse(true); + ->reverseConstantArray(TrinaryLogic::createYes()); } if ($offset > 0) { @@ -1162,16 +1179,10 @@ public function slice(int $offset, ?int $limit, bool $preserveKeys = false): sel return $preserveKeys ? $slice : $slice->reindex(); } + /** @deprecated Use reverseArray() instead */ public function reverse(bool $preserveKeys = false): self { - $keyTypesReversed = array_reverse($this->keyTypes, true); - $keyTypes = array_values($keyTypesReversed); - $keyTypesReversedKeys = array_keys($keyTypesReversed); - $optionalKeys = array_map(static fn (int $optionalKey): int => $keyTypesReversedKeys[$optionalKey], $this->optionalKeys); - - $reversed = new self($keyTypes, array_reverse($this->valueTypes), $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo()); - - return $preserveKeys ? $reversed : $reversed->reindex(); + return $this->reverseConstantArray(TrinaryLogic::createFromBoolean($preserveKeys)); } /** @param positive-int $length */ diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 9b4fbf22ba..08ff407033 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -744,6 +744,11 @@ public function popArray(): Type return $this->intersectTypes(static fn (Type $type): Type => $type->popArray()); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); + } + public function searchArray(Type $needleType): Type { return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType)); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 402d192a7d..c45642f0d2 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -225,6 +225,15 @@ public function popArray(): Type return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + public function searchArray(Type $needleType): Type { if ($this->isArray()->no()) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index b619768b6d..a21dcb7d23 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -316,6 +316,11 @@ public function popArray(): Type return new NeverType(); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return new NeverType(); + } + public function searchArray(Type $needleType): Type { return new NeverType(); diff --git a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php index 1a693eff8d..1696d39d48 100644 --- a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php @@ -4,16 +4,21 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use function count; final class ArrayReverseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_reverse'; @@ -26,24 +31,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $type = $scope->getType($functionCall->getArgs()[0]->value); - $preserveKeysType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : new NeverType(); - $preserveKeys = $preserveKeysType->isTrue()->yes(); - - if (!$type->isArray()->yes()) { - return null; + if ($type->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - $constantArrays = $type->getConstantArrays(); - if (count($constantArrays) > 0) { - $results = []; - foreach ($constantArrays as $constantArray) { - $results[] = $constantArray->reverse($preserveKeys); - } - - return TypeCombinator::union(...$results); - } + $preserveKeysType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : new ConstantBooleanType(false); + $preserveKeys = (new ConstantBooleanType(true))->isSuperTypeOf($preserveKeysType); - return $type; + return $type->reverseArray($preserveKeys); } } diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 766cede665..9db6c41cd0 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -435,6 +435,11 @@ public function popArray(): Type return $this->getStaticObjectType()->popArray(); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->getStaticObjectType()->reverseArray($preserveKeys); + } + public function searchArray(Type $needleType): Type { return $this->getStaticObjectType()->searchArray($needleType); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index a749a6fae0..bea1d953a0 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -282,6 +282,11 @@ public function popArray(): Type return $this->resolve()->popArray(); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->resolve()->reverseArray($preserveKeys); + } + public function searchArray(Type $needleType): Type { return $this->resolve()->searchArray($needleType); diff --git a/src/Type/Traits/MaybeArrayTypeTrait.php b/src/Type/Traits/MaybeArrayTypeTrait.php index da064c82b7..7d25897553 100644 --- a/src/Type/Traits/MaybeArrayTypeTrait.php +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -69,6 +69,11 @@ public function popArray(): Type return new ErrorType(); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + public function searchArray(Type $needleType): Type { return new ErrorType(); diff --git a/src/Type/Traits/NonArrayTypeTrait.php b/src/Type/Traits/NonArrayTypeTrait.php index def99b0e94..8deb186895 100644 --- a/src/Type/Traits/NonArrayTypeTrait.php +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -69,6 +69,11 @@ public function popArray(): Type return new ErrorType(); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return new ErrorType(); + } + public function searchArray(Type $needleType): Type { return new ErrorType(); diff --git a/src/Type/Type.php b/src/Type/Type.php index ec682c3e03..60e1046d1b 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -159,6 +159,8 @@ public function intersectKeyArray(Type $otherArraysType): Type; public function popArray(): Type; + public function reverseArray(TrinaryLogic $preserveKeys): Type; + public function searchArray(Type $needleType): Type; public function shiftArray(): Type; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 1a8ac35969..f79dbe88a4 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -721,6 +721,11 @@ public function popArray(): Type return $this->unionTypes(static fn (Type $type): Type => $type->popArray()); } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); + } + public function searchArray(Type $needleType): Type { return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType)); diff --git a/tests/PHPStan/Analyser/nsrt/array-reverse-php7.php b/tests/PHPStan/Analyser/nsrt/array-reverse-php7.php new file mode 100644 index 0000000000..4c9d1ab563 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-reverse-php7.php @@ -0,0 +1,16 @@ += 8.0 + +declare(strict_types = 1); + +namespace ArrayReversePhp8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function notArray(bool $bool): void + { + assertType('*NEVER*', array_reverse($bool)); + assertType('*NEVER*', array_reverse($bool, true)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-reverse.php b/tests/PHPStan/Analyser/nsrt/array-reverse.php index e5a205ac0a..413e1d5f2a 100644 --- a/tests/PHPStan/Analyser/nsrt/array-reverse.php +++ b/tests/PHPStan/Analyser/nsrt/array-reverse.php @@ -46,4 +46,28 @@ public function constantArrays(array $a, array $b): void assertType('array{\'bar\', \'foo\'}|array{bar: 19, foo: 17}', array_reverse($b)); assertType('array{19: \'bar\', 17: \'foo\'}|array{bar: 19, foo: 17}', array_reverse($b, true)); } + + /** + * @param list $a + * @param non-empty-list $b + */ + public function list(array $a, array $b): void + { + assertType('list', array_reverse($a)); + assertType('array, string>', array_reverse($a, true)); + + assertType('non-empty-list', array_reverse($b)); + assertType('non-empty-array, string>', array_reverse($b, true)); + } + + public function mixed(mixed $mixed): void + { + assertType('array', array_reverse($mixed)); + assertType('array', array_reverse($mixed, true)); + + if (array_key_exists('foo', $mixed)) { + assertType('non-empty-array', array_reverse($mixed)); + assertType("array&hasOffset('foo')", array_reverse($mixed, true)); + } + } }