From 4863c885a2836a47d5475f48bc63fce64b4550c6 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 7 May 2026 21:14:14 +0200 Subject: [PATCH 1/2] Extract `Type::sortArray()` for `sort` / `rsort` / `usort` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces `FuncCallHandler::getArraySortPreserveListFunctionType()`'s hand-rolled `TypeTraverser::map` callback with a polymorphic `Type` method. The call site shrinks to `$type->sortArray()`. The semantics — values reordered, array re-indexed as a list — are similar to `shuffleArray()` but differ in two ways that ruled out direct reuse: - Empty arrays stay empty (`array{}` does not degrade to `list`). The check moves into `ArrayTypeTrait::sortArray()` via `isIterableAtLeastOnce()->no()`, so per-leaf decisions also preserve emptiness inside unions (precision improvement vs. the closure-captured outer flag in the original). - Non-array leaves pass through unchanged (rather than `ErrorType` like `shuffleArray()`), because `sort($x)` where `$x` is a `mixed` / template / non-array variable should not erase the variable's type — the arg-type rule is responsible for the diagnostic. Implementation: - `ArrayTypeTrait::sortArray()`: shared body for `ArrayType` and `ConstantArrayType`. Returns the input unchanged if `isIterableAtLeastOnce()->no()`; otherwise builds `IntersectionType[ArrayType, valueType>, AccessoryArrayListType]`, intersected with `NonEmptyArrayType` when the leaf's own `isIterableAtLeastOnce()` is `Yes`. - `NonArrayTypeTrait`, `MaybeArrayTypeTrait`, `MixedType`, `StrictMixedType`, `StaticType`, `NeverType`, and the array accessory types (`AccessoryArrayListType`, `NonEmptyArrayType`, `OversizedArrayType`, `HasOffsetType`, `HasOffsetValueType`): return `$this`. - `UnionType` / `IntersectionType`: distribute via `unionTypes` / `intersectTypes`. - `LateResolvableTypeTrait`: delegates to `resolve()`. Pure refactor: full test suite + phpstan + cs pass. --- src/Analyser/ExprHandler/FuncCallHandler.php | 28 +------------------ src/Type/Accessory/AccessoryArrayListType.php | 5 ++++ src/Type/Accessory/HasOffsetType.php | 5 ++++ src/Type/Accessory/HasOffsetValueType.php | 5 ++++ src/Type/Accessory/NonEmptyArrayType.php | 5 ++++ src/Type/Accessory/OversizedArrayType.php | 5 ++++ src/Type/IntersectionType.php | 5 ++++ src/Type/MixedType.php | 5 ++++ src/Type/NeverType.php | 5 ++++ src/Type/StaticType.php | 5 ++++ src/Type/Traits/ArrayTypeTrait.php | 19 +++++++++++++ src/Type/Traits/LateResolvableTypeTrait.php | 5 ++++ src/Type/Traits/MaybeArrayTypeTrait.php | 5 ++++ src/Type/Traits/NonArrayTypeTrait.php | 5 ++++ src/Type/Type.php | 8 ++++++ src/Type/UnionType.php | 5 ++++ 16 files changed, 93 insertions(+), 27 deletions(-) diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 3e9682ad5a..89762a2373 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; @@ -462,7 +461,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(); } @@ -722,31 +721,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(); 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)); From 1fbad554e263e8ae946c27e3f18589fa941e7aa1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 7 May 2026 21:25:53 +0200 Subject: [PATCH 2/2] Inline one-line wrappers around polymorphic Type methods Follow-up cleanup across #5611, #5612, #5613: - `FuncCallHandler::getArrayWalkResultType()` and `getArraySortDoNotPreserveListFunctionType()` were single-statement wrappers around `Type::mapValueType()` / `Type::makeListMaybe()`. Both private, both fully replaced by their polymorphic call; inline at the two/one call sites and delete the helpers. - `InstanceofHandler` and `TypeSpecifier` (the `instanceof ` branch) used a throwaway `$classType = $scope->getType(...)` only to immediately overwrite it with `$result->type`. Drop the intermediate; chain the call. Pure simplification: full test suite + phpstan + cs pass. --- src/Analyser/ExprHandler/FuncCallHandler.php | 18 +++++------------- src/Analyser/ExprHandler/InstanceofHandler.php | 3 +-- src/Analyser/TypeSpecifier.php | 3 +-- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 89762a2373..6ceda2b068 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -273,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, @@ -478,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(); } @@ -721,16 +723,6 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra return $arrayType; } - 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;