From 749d403e0a16655cc7e1eaa2f46b8a7791af1af0 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Mon, 18 May 2026 10:52:17 +0000 Subject: [PATCH 1/6] Preserve `TemplateType` in `ArrayType` and `IntersectionType` array-mutating methods - In `ArrayType`, use `$this->withTypes()` instead of `new self()` / `new ArrayType()` in `intersectKeyArray()`, `sliceArray()`, `spliceArray()`, `mapValueType()`, `mapKeyType()`, `changeKeyCaseArray()`, and `filterArrayRemovingFalsey()` so that `TemplateArrayType` preserves its template identity through these operations. - In `IntersectionType`, skip calling array methods on `TemplateType` members (return the template unchanged) in `popArray()`, `shiftArray()`, `reverseArray()`, `sliceArray()`, `spliceArray()`, `intersectKeyArray()`, `mapValueType()`, `mapKeyType()`, `changeKeyCaseArray()`, `filterArrayRemovingFalsey()`, and `getValuesArray()` (conditional on list-ness for the latter). - In `ArrayFilterFunctionReturnTypeHelper`, call `filterArrayRemovingFalsey()` directly on the original type when it is purely an array, preserving intersection context (and thus template types) instead of decomposing via `getArrays()`. --- src/Type/ArrayType.php | 14 +- src/Type/IntersectionType.php | 78 +++++++-- .../ArrayFilterFunctionReturnTypeHelper.php | 7 +- tests/PHPStan/Analyser/nsrt/bug-14633.php | 164 ++++++++++++++++++ 4 files changed, 243 insertions(+), 20 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14633.php diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 7f3eed6f821..978e9d6ca87 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -493,7 +493,7 @@ public function intersectKeyArray(Type $otherArraysType): Type return $this; } - return new self($otherArraysType->getIterableKeyType(), $this->getIterableValueType()); + return $this->withTypes($otherArraysType->getIterableKeyType(), $this->getIterableValueType()); } public function popArray(): Type @@ -533,7 +533,7 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre } if ($preserveKeys->no() && $this->keyType->isInteger()->yes()) { - return new IntersectionType([new self(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $this->itemType), new AccessoryArrayListType()]); + return new IntersectionType([$this->withTypes(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $this->itemType), new AccessoryArrayListType()]); } return $this; @@ -561,7 +561,7 @@ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacemen return $type; }); - $arrayType = new self( + $arrayType = $this->withTypes( TypeCombinator::union($keyType, $replacementArrayType->getKeysArray()->getIterableKeyType()), TypeCombinator::union($this->getIterableValueType(), $replacementArrayType->getIterableValueType()), ); @@ -591,12 +591,12 @@ public function makeListMaybe(): Type public function mapValueType(callable $cb): Type { - return new ArrayType($this->keyType, $cb($this->getItemType())); + return $this->withTypes($this->keyType, $cb($this->getItemType())); } public function mapKeyType(callable $cb): Type { - return new ArrayType($cb($this->keyType), $this->getItemType()); + return $this->withTypes($cb($this->keyType), $this->getItemType()); } public function makeAllArrayKeysOptional(): Type @@ -647,7 +647,7 @@ public function changeKeyCaseArray(?int $case): Type return $type; }); - return new ArrayType($newKeyType, $this->getItemType()); + return $this->withTypes($newKeyType, $this->getItemType()); } public function filterArrayRemovingFalsey(): Type @@ -658,7 +658,7 @@ public function filterArrayRemovingFalsey(): Type return new ConstantArrayType([], []); } - return new ArrayType($this->keyType, $valueType); + return $this->withTypes($this->keyType, $valueType); } private static function foldConstantStringKeyCase(ConstantStringType $type, ?int $case): Type diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index b65b75b274f..b6842953a29 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -1151,7 +1151,13 @@ public function getKeysArray(): Type public function getValuesArray(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray()); + $isList = $this->isList()->yes(); + return $this->intersectTypes(static function (Type $type) use ($isList): Type { + if ($isList && $type instanceof TemplateType) { + return $type; + } + return $type->getValuesArray(); + }); } public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type @@ -1171,17 +1177,32 @@ public function flipArray(): Type public function intersectKeyArray(Type $otherArraysType): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType)); + return $this->intersectTypes(static function (Type $type) use ($otherArraysType): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $type->intersectKeyArray($otherArraysType); + }); } public function popArray(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->popArray()); + return $this->intersectTypes(static function (Type $type): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $type->popArray(); + }); } public function reverseArray(TrinaryLogic $preserveKeys): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); + return $this->intersectTypes(static function (Type $type) use ($preserveKeys): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $type->reverseArray($preserveKeys); + }); } public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type @@ -1191,7 +1212,12 @@ public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Typ public function shiftArray(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->shiftArray()); + return $this->intersectTypes(static function (Type $type): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $type->shiftArray(); + }); } public function shuffleArray(): Type @@ -1207,7 +1233,12 @@ public function shuffleArray(): Type public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { - $result = $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); + $result = $this->intersectTypes(static function (Type $type) use ($offsetType, $lengthType, $preserveKeys): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $type->sliceArray($offsetType, $lengthType, $preserveKeys); + }); if ( $this->isList()->yes() @@ -1223,7 +1254,12 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType)); + return $this->intersectTypes(static function (Type $type) use ($offsetType, $lengthType, $replacementType): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $type->spliceArray($offsetType, $lengthType, $replacementType); + }); } public function makeListMaybe(): Type @@ -1233,12 +1269,22 @@ public function makeListMaybe(): Type public function mapValueType(callable $cb): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->mapValueType($cb)); + return $this->intersectTypes(static function (Type $type) use ($cb): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $type->mapValueType($cb); + }); } public function mapKeyType(callable $cb): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->mapKeyType($cb)); + return $this->intersectTypes(static function (Type $type) use ($cb): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $type->mapKeyType($cb); + }); } public function makeAllArrayKeysOptional(): Type @@ -1248,12 +1294,22 @@ public function makeAllArrayKeysOptional(): Type public function changeKeyCaseArray(?int $case): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->changeKeyCaseArray($case)); + return $this->intersectTypes(static function (Type $type) use ($case): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $type->changeKeyCaseArray($case); + }); } public function filterArrayRemovingFalsey(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->filterArrayRemovingFalsey()); + return $this->intersectTypes(static function (Type $type): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $type->filterArrayRemovingFalsey(); + }); } public function getEnumCases(): array diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php index 16e03fd68aa..495ed597969 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php @@ -60,8 +60,8 @@ public function getType(Scope $scope, ?Expr $arrayArg, ?Expr $callbackArg, ?Expr return new ArrayType(new MixedType(), new MixedType()); } - $arrayArgType = $scope->getType($arrayArg); - $arrayArgType = TypeUtils::toBenevolentUnion($arrayArgType); + $originalArrayArgType = $scope->getType($arrayArg); + $arrayArgType = TypeUtils::toBenevolentUnion($originalArrayArgType); $keyType = $arrayArgType->getIterableKeyType(); $itemType = $arrayArgType->getIterableValueType(); @@ -81,6 +81,9 @@ public function getType(Scope $scope, ?Expr $arrayArg, ?Expr $callbackArg, ?Expr } if ($callbackArg === null || $scope->getType($callbackArg)->isNull()->yes()) { + if ($originalArrayArgType->isArray()->yes()) { + return $originalArrayArgType->filterArrayRemovingFalsey(); + } return TypeCombinator::union( ...array_map([$this, 'removeFalsey'], $arrayArgType->getArrays()), ); diff --git a/tests/PHPStan/Analyser/nsrt/bug-14633.php b/tests/PHPStan/Analyser/nsrt/bug-14633.php new file mode 100644 index 00000000000..5c5950297ff --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14633.php @@ -0,0 +1,164 @@ + or T&array + */ +class IntersectionTemplatePreservation +{ + + /** + * @template T + * @param T&list $items + */ + public function popList(array $items): void + { + array_pop($items); + assertType('list&T (method Bug14633\IntersectionTemplatePreservation::popList(), argument)', $items); + } + + /** + * @template T + * @param T&array $items + */ + public function popArray(array $items): void + { + array_pop($items); + assertType('array&T (method Bug14633\IntersectionTemplatePreservation::popArray(), argument)', $items); + } + + /** + * @template T + * @param T&list $items + */ + public function shiftList(array $items): void + { + array_shift($items); + assertType('list&T (method Bug14633\IntersectionTemplatePreservation::shiftList(), argument)', $items); + } + + /** + * @template T + * @param T&array $items + */ + public function shiftArray(array $items): void + { + array_shift($items); + assertType('array&T (method Bug14633\IntersectionTemplatePreservation::shiftArray(), argument)', $items); + } + + /** + * @template T + * @param T&list $items + */ + public function reverseList(array $items): void + { + $reversed = array_reverse($items); + assertType('list&T (method Bug14633\IntersectionTemplatePreservation::reverseList(), argument)', $reversed); + } + + /** + * @template T + * @param T&array $items + */ + public function reverseArrayPreserveKeys(array $items): void + { + $reversed = array_reverse($items, true); + assertType('array&T (method Bug14633\IntersectionTemplatePreservation::reverseArrayPreserveKeys(), argument)', $reversed); + } + + /** + * @template T + * @param T&list $items + */ + public function sliceList(array $items): void + { + $sliced = array_slice($items, 1); + assertType('list&T (method Bug14633\IntersectionTemplatePreservation::sliceList(), argument)', $sliced); + } + + /** + * @template T + * @param T&array $items + */ + public function sliceArrayPreserveKeys(array $items): void + { + $sliced = array_slice($items, 0, 5, true); + assertType('array&T (method Bug14633\IntersectionTemplatePreservation::sliceArrayPreserveKeys(), argument)', $sliced); + } + + /** + * @template T + * @param T&list $items + */ + public function arrayValuesOnList(array $items): void + { + $values = array_values($items); + assertType('list&T (method Bug14633\IntersectionTemplatePreservation::arrayValuesOnList(), argument)', $values); + } + + /** + * @template T + * @param T&list $items + */ + public function arrayFilterOnList(array $items): void + { + $filtered = array_filter($items); + assertType('array, int|int<1, max>>&T (method Bug14633\IntersectionTemplatePreservation::arrayFilterOnList(), argument)', $filtered); + } + + /** + * @template T + * @param T&array $items + */ + public function arrayFilterOnArray(array $items): void + { + $filtered = array_filter($items); + assertType('array|int<1, max>>&T (method Bug14633\IntersectionTemplatePreservation::arrayFilterOnArray(), argument)', $filtered); + } + +} + +/** + * Tests for ArrayType methods preserving template via $this->withTypes(). + * Pattern: @template T of array + */ +class ArrayTypeTemplatePreservation +{ + + /** + * @template T of array + * @param T $items + */ + public function filterArrayRemovingFalsey(array $items): void + { + $result = array_filter($items); + assertType('T of array|int<1, max>> (method Bug14633\ArrayTypeTemplatePreservation::filterArrayRemovingFalsey(), argument)', $result); + } + + /** + * @template T of array + * @param T $items + * @param array $other + */ + public function intersectKeyArray(array $items, array $other): void + { + $result = array_intersect_key($items, $other); + assertType('T of array (method Bug14633\ArrayTypeTemplatePreservation::intersectKeyArray(), argument)', $result); + } + + /** + * @template T of array + * @param T $items + */ + public function sliceArray(array $items): void + { + $result = array_slice($items, 1); + assertType('T of array, int> (method Bug14633\ArrayTypeTemplatePreservation::sliceArray(), argument)&list', $result); + } + +} From 5ca56eae0911fdb516fe409ab3653376c69d9acf Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 18 May 2026 11:51:44 +0000 Subject: [PATCH 2/6] Extract `intersectTypesPreserveTemplateType()` helper in `IntersectionType` Co-Authored-By: Claude Opus 4.6 --- src/Type/IntersectionType.php | 83 ++++++++++------------------------- 1 file changed, 23 insertions(+), 60 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index b6842953a29..a0620bcb30c 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -1177,32 +1177,17 @@ public function flipArray(): Type public function intersectKeyArray(Type $otherArraysType): Type { - return $this->intersectTypes(static function (Type $type) use ($otherArraysType): Type { - if ($type instanceof TemplateType) { - return $type; - } - return $type->intersectKeyArray($otherArraysType); - }); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType)); } public function popArray(): Type { - return $this->intersectTypes(static function (Type $type): Type { - if ($type instanceof TemplateType) { - return $type; - } - return $type->popArray(); - }); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->popArray()); } public function reverseArray(TrinaryLogic $preserveKeys): Type { - return $this->intersectTypes(static function (Type $type) use ($preserveKeys): Type { - if ($type instanceof TemplateType) { - return $type; - } - return $type->reverseArray($preserveKeys); - }); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); } public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type @@ -1212,12 +1197,7 @@ public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Typ public function shiftArray(): Type { - return $this->intersectTypes(static function (Type $type): Type { - if ($type instanceof TemplateType) { - return $type; - } - return $type->shiftArray(); - }); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->shiftArray()); } public function shuffleArray(): Type @@ -1233,12 +1213,7 @@ public function shuffleArray(): Type public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { - $result = $this->intersectTypes(static function (Type $type) use ($offsetType, $lengthType, $preserveKeys): Type { - if ($type instanceof TemplateType) { - return $type; - } - return $type->sliceArray($offsetType, $lengthType, $preserveKeys); - }); + $result = $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); if ( $this->isList()->yes() @@ -1254,12 +1229,7 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type { - return $this->intersectTypes(static function (Type $type) use ($offsetType, $lengthType, $replacementType): Type { - if ($type instanceof TemplateType) { - return $type; - } - return $type->spliceArray($offsetType, $lengthType, $replacementType); - }); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType)); } public function makeListMaybe(): Type @@ -1269,22 +1239,12 @@ public function makeListMaybe(): Type public function mapValueType(callable $cb): Type { - return $this->intersectTypes(static function (Type $type) use ($cb): Type { - if ($type instanceof TemplateType) { - return $type; - } - return $type->mapValueType($cb); - }); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->mapValueType($cb)); } public function mapKeyType(callable $cb): Type { - return $this->intersectTypes(static function (Type $type) use ($cb): Type { - if ($type instanceof TemplateType) { - return $type; - } - return $type->mapKeyType($cb); - }); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->mapKeyType($cb)); } public function makeAllArrayKeysOptional(): Type @@ -1294,22 +1254,12 @@ public function makeAllArrayKeysOptional(): Type public function changeKeyCaseArray(?int $case): Type { - return $this->intersectTypes(static function (Type $type) use ($case): Type { - if ($type instanceof TemplateType) { - return $type; - } - return $type->changeKeyCaseArray($case); - }); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->changeKeyCaseArray($case)); } public function filterArrayRemovingFalsey(): Type { - return $this->intersectTypes(static function (Type $type): Type { - if ($type instanceof TemplateType) { - return $type; - } - return $type->filterArrayRemovingFalsey(); - }); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->filterArrayRemovingFalsey()); } public function getEnumCases(): array @@ -1770,6 +1720,19 @@ private function intersectTypes(callable $getType): Type return $result; } + /** + * @param callable(Type $type): Type $getType + */ + private function intersectTypesPreserveTemplateType(callable $getType): Type + { + return $this->intersectTypes(static function (Type $type) use ($getType): Type { + if ($type instanceof TemplateType) { + return $type; + } + return $getType($type); + }); + } + public function toPhpDocNode(): TypeNode { $baseTypes = []; From 91eff285beaf6a6506b5c2aafab6539a1b77b3a7 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 18 May 2026 12:13:00 +0000 Subject: [PATCH 3/6] Add tests verifying template types are only preserved on lists for getValuesArray/shuffleArray For array_values() and shuffle() on non-list arrays (e.g. T & array), the template type T must NOT be preserved because keys change from string to int. On lists, T is correctly preserved since the key structure is unchanged. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-14633.php | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14633.php b/tests/PHPStan/Analyser/nsrt/bug-14633.php index 5c5950297ff..a444badff8d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14633.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14633.php @@ -101,6 +101,42 @@ public function arrayValuesOnList(array $items): void assertType('list&T (method Bug14633\IntersectionTemplatePreservation::arrayValuesOnList(), argument)', $values); } + /** + * array_values() on a non-list must NOT preserve T — keys change from string to int. + * + * @template T + * @param T&array $items + */ + public function arrayValuesOnNonList(array $items): void + { + $values = array_values($items); + assertType('list', $values); + } + + /** + * shuffle() on a list preserves T — keys are already sequential integers. + * + * @template T + * @param T&list $items + */ + public function shuffleOnList(array $items): void + { + shuffle($items); + assertType('list&T (method Bug14633\IntersectionTemplatePreservation::shuffleOnList(), argument)', $items); + } + + /** + * shuffle() on a non-list must NOT preserve T — keys change from string to int. + * + * @template T + * @param T&array $items + */ + public function shuffleOnNonList(array $items): void + { + shuffle($items); + assertType('list', $items); + } + /** * @template T * @param T&list $items From f4baea8b470133d8180ce86a1778afa92d56c81a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 18 May 2026 12:14:24 +0000 Subject: [PATCH 4/6] Use `intersectTypesPreserveTemplateType` conditionally in `getValuesArray` and `shuffleArray` Instead of inlining the TemplateType check inside the closure, branch on `$isList` and call either `intersectTypesPreserveTemplateType` or `intersectTypes` directly. This is cleaner and consistent with how the other methods use the helper. Co-Authored-By: Claude Opus 4.6 --- src/Type/IntersectionType.php | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index a0620bcb30c..a42b80a545b 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -1151,13 +1151,11 @@ public function getKeysArray(): Type public function getValuesArray(): Type { - $isList = $this->isList()->yes(); - return $this->intersectTypes(static function (Type $type) use ($isList): Type { - if ($isList && $type instanceof TemplateType) { - return $type; - } - return $type->getValuesArray(); - }); + $cb = static fn (Type $type): Type => $type->getValuesArray(); + if ($this->isList()->yes()) { + return $this->intersectTypesPreserveTemplateType($cb); + } + return $this->intersectTypes($cb); } public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type @@ -1202,13 +1200,11 @@ public function shiftArray(): Type public function shuffleArray(): Type { - $isList = $this->isList()->yes(); - return $this->intersectTypes(static function (Type $type) use ($isList): Type { - if ($isList && $type instanceof TemplateType) { - return $type; - } - return $type->shuffleArray(); - }); + $cb = static fn (Type $type): Type => $type->shuffleArray(); + if ($this->isList()->yes()) { + return $this->intersectTypesPreserveTemplateType($cb); + } + return $this->intersectTypes($cb); } public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type From 212a920abf8bc2525cf8abc5698172b9fa7ff3ce Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 18 May 2026 14:16:35 +0200 Subject: [PATCH 5/6] Update IntersectionType.php --- src/Type/IntersectionType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index a42b80a545b..56762ae30c5 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -1153,7 +1153,7 @@ public function getValuesArray(): Type { $cb = static fn (Type $type): Type => $type->getValuesArray(); if ($this->isList()->yes()) { - return $this->intersectTypesPreserveTemplateType($cb); + return $this; } return $this->intersectTypes($cb); } From 9d7d527e5f5b63940f164d519f3d559dabadc83f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 18 May 2026 14:25:01 +0000 Subject: [PATCH 6/6] Revert ArrayFilterFunctionReturnTypeHelper changes and related assertions The template-preserving shortcut in ArrayFilterFunctionReturnTypeHelper is a separate concern from the ArrayType/IntersectionType fixes and deserves its own PR. Co-Authored-By: Claude Opus 4.6 --- .../ArrayFilterFunctionReturnTypeHelper.php | 7 ++----- tests/PHPStan/Analyser/nsrt/bug-14633.php | 20 ------------------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php index 495ed597969..16e03fd68aa 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php @@ -60,8 +60,8 @@ public function getType(Scope $scope, ?Expr $arrayArg, ?Expr $callbackArg, ?Expr return new ArrayType(new MixedType(), new MixedType()); } - $originalArrayArgType = $scope->getType($arrayArg); - $arrayArgType = TypeUtils::toBenevolentUnion($originalArrayArgType); + $arrayArgType = $scope->getType($arrayArg); + $arrayArgType = TypeUtils::toBenevolentUnion($arrayArgType); $keyType = $arrayArgType->getIterableKeyType(); $itemType = $arrayArgType->getIterableValueType(); @@ -81,9 +81,6 @@ public function getType(Scope $scope, ?Expr $arrayArg, ?Expr $callbackArg, ?Expr } if ($callbackArg === null || $scope->getType($callbackArg)->isNull()->yes()) { - if ($originalArrayArgType->isArray()->yes()) { - return $originalArrayArgType->filterArrayRemovingFalsey(); - } return TypeCombinator::union( ...array_map([$this, 'removeFalsey'], $arrayArgType->getArrays()), ); diff --git a/tests/PHPStan/Analyser/nsrt/bug-14633.php b/tests/PHPStan/Analyser/nsrt/bug-14633.php index a444badff8d..6c2f3a6a8c0 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14633.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14633.php @@ -137,26 +137,6 @@ public function shuffleOnNonList(array $items): void assertType('list', $items); } - /** - * @template T - * @param T&list $items - */ - public function arrayFilterOnList(array $items): void - { - $filtered = array_filter($items); - assertType('array, int|int<1, max>>&T (method Bug14633\IntersectionTemplatePreservation::arrayFilterOnList(), argument)', $filtered); - } - - /** - * @template T - * @param T&array $items - */ - public function arrayFilterOnArray(array $items): void - { - $filtered = array_filter($items); - assertType('array|int<1, max>>&T (method Bug14633\IntersectionTemplatePreservation::arrayFilterOnArray(), argument)', $filtered); - } - } /**