diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 3e9682ad5a..6ceda2b068 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -60,7 +60,6 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; use Throwable; use function array_filter; @@ -274,8 +273,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); if ($arrayWalkValueTypes !== null && $arrayWalkArrayArg !== null) { - $newArrayType = $this->getArrayWalkResultType($arrayWalkOriginalArrayType, $arrayWalkValueTypes[0]); - $newArrayNativeType = $this->getArrayWalkResultType($arrayWalkOriginalArrayNativeType, $arrayWalkValueTypes[1]); + $arrayWalkValueType = $arrayWalkValueTypes[0]; + $arrayWalkValueNativeType = $arrayWalkValueTypes[1]; + $newArrayType = $arrayWalkOriginalArrayType->mapValueType(static fn (Type $type): Type => $arrayWalkValueType); + $newArrayNativeType = $arrayWalkOriginalArrayNativeType->mapValueType(static fn (Type $type): Type => $arrayWalkValueNativeType); $scope = $nodeScopeResolver->processVirtualAssign( $scope, @@ -462,7 +463,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $storage, $stmt, $arrayArg, - new NativeTypeExpr($this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)), $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg))), + new NativeTypeExpr($scope->getType($arrayArg)->sortArray(), $scope->getNativeType($arrayArg)->sortArray()), $nodeCallback, )->getScope(); } @@ -479,7 +480,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $storage, $stmt, $arrayArg, - new NativeTypeExpr($this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)), $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg))), + new NativeTypeExpr($scope->getType($arrayArg)->makeListMaybe(), $scope->getNativeType($arrayArg)->makeListMaybe()), $nodeCallback, )->getScope(); } @@ -722,41 +723,6 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra return $arrayType; } - private function getArraySortPreserveListFunctionType(Type $type): Type - { - $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); - if ($isIterableAtLeastOnce->no()) { - return $type; - } - - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - - if (!$type instanceof ArrayType && !$type instanceof ConstantArrayType) { - return $type; - } - - $newArrayType = new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $type->getIterableValueType()), new AccessoryArrayListType()]); - if ($isIterableAtLeastOnce->yes()) { - $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); - } - - return $newArrayType; - }); - } - - private function getArraySortDoNotPreserveListFunctionType(Type $type): Type - { - return $type->makeListMaybe(); - } - - private function getArrayWalkResultType(Type $arrayType, Type $newValueType): Type - { - return $arrayType->mapValueType(static fn (Type $type): Type => $newValueType); - } - public function resolveType(MutatingScope $scope, Expr $expr): Type { if ($expr->name instanceof Expr) { diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index ffc2045c58..e015ce1e70 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -91,8 +91,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type $classType = new ObjectType($className); } } else { - $classType = $scope->getType($expr->class); - $result = $classType->toObjectTypeForInstanceofCheck(); + $result = $scope->getType($expr->class)->toObjectTypeForInstanceofCheck(); $classType = $result->type; $uncertainty = $result->uncertainty; } diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 973826839c..5ae4060811 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -164,8 +164,7 @@ public function specifyTypesInCondition( return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr); } - $classType = $scope->getType($expr->class); - $result = $classType->toObjectTypeForInstanceofCheck(); + $result = $scope->getType($expr->class)->toObjectTypeForInstanceofCheck(); $type = $result->type; $uncertainty = $result->uncertainty; diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index a5b4a02db2..19b8d5543e 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -242,6 +242,11 @@ public function shuffleArray(): Type return $this; } + public function sortArray(): Type + { + return $this; + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { if ($preserveKeys->no()) { diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 313de6aeb0..d0c5740db2 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -202,6 +202,11 @@ public function shuffleArray(): Type return new NonEmptyArrayType(); } + public function sortArray(): Type + { + return $this; + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { if ( diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index d20ab5c916..1ae12e2ced 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -291,6 +291,11 @@ public function shuffleArray(): Type return new NonEmptyArrayType(); } + public function sortArray(): Type + { + return $this; + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { if ( diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 2e8afa85e1..6735ce601e 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -226,6 +226,11 @@ public function shuffleArray(): Type return $this; } + public function sortArray(): Type + { + return $this; + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() && $lengthType->isNull()->yes()) { diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index 03c26560b6..d959a02e18 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -214,6 +214,11 @@ public function shuffleArray(): Type return $this; } + public function sortArray(): Type + { + return $this; + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { return $this; diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 4b45e2453a..1c0b3a3247 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -1164,6 +1164,11 @@ public function shuffleArray(): Type return $this->intersectTypes(static fn (Type $type): Type => $type->shuffleArray()); } + public function sortArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->sortArray()); + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { $result = $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 8fee88984d..8bb6e441c2 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -288,6 +288,11 @@ public function shuffleArray(): Type return new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType($this->isExplicitMixed)), new AccessoryArrayListType()]); } + public function sortArray(): Type + { + return $this; + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { if ($this->isArray()->no()) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index c73d5813a6..eb56bcf7a3 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -375,6 +375,11 @@ public function shuffleArray(): Type return new NeverType(); } + public function sortArray(): Type + { + return $this; + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { return new NeverType(); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index ff646bb533..4a2f12c8f2 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -550,6 +550,11 @@ public function shuffleArray(): Type return $this->getStaticObjectType()->shuffleArray(); } + public function sortArray(): Type + { + return $this; + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { return $this->getStaticObjectType()->sliceArray($offsetType, $lengthType, $preserveKeys); diff --git a/src/Type/Traits/ArrayTypeTrait.php b/src/Type/Traits/ArrayTypeTrait.php index 9ac5e1d97d..e312de17af 100644 --- a/src/Type/Traits/ArrayTypeTrait.php +++ b/src/Type/Traits/ArrayTypeTrait.php @@ -214,4 +214,23 @@ public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type : $arrayType; } + public function sortArray(): Type + { + $isIterableAtLeastOnce = $this->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no()) { + return $this; + } + + $listType = new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $this->getIterableValueType()), + new AccessoryArrayListType(), + ]); + + if ($isIterableAtLeastOnce->yes()) { + $listType = TypeCombinator::intersect($listType, new NonEmptyArrayType()); + } + + return $listType; + } + } diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index e5d2b58373..de7e86c0b8 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -346,6 +346,11 @@ public function shuffleArray(): Type return $this->resolve()->shuffleArray(); } + public function sortArray(): Type + { + return $this->resolve()->sortArray(); + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { return $this->resolve()->sliceArray($offsetType, $lengthType, $preserveKeys); diff --git a/src/Type/Traits/MaybeArrayTypeTrait.php b/src/Type/Traits/MaybeArrayTypeTrait.php index 9b87c858ca..2827482305 100644 --- a/src/Type/Traits/MaybeArrayTypeTrait.php +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -99,6 +99,11 @@ public function shuffleArray(): Type return new ErrorType(); } + public function sortArray(): Type + { + return $this; + } + 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 eb06353c49..296a293631 100644 --- a/src/Type/Traits/NonArrayTypeTrait.php +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -99,6 +99,11 @@ public function shuffleArray(): Type return new ErrorType(); } + public function sortArray(): Type + { + return $this; + } + 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 9eed2d7140..7655ccf565 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -273,6 +273,14 @@ public function shiftArray(): Type; /** Models shuffle() effect on the array. Result is always a list. */ public function shuffleArray(): Type; + /** + * Models `sort` / `rsort` / `usort`: values are reordered and the array + * is re-indexed as a list. Empty arrays stay empty; non-array leaves + * pass through unchanged (the call site is responsible for arg-type + * checks). + */ + public function sortArray(): Type; + /** Models array_slice($array, $offset, $length, $preserveKeys). */ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index f69dff5768..9ebacb2266 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -895,6 +895,11 @@ public function shuffleArray(): Type return $this->unionTypes(static fn (Type $type): Type => $type->shuffleArray()); } + public function sortArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->sortArray()); + } + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { return $this->unionTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));