diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 7f3eed6f82..978e9d6ca8 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 b65b75b274..56762ae30c 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -1151,7 +1151,11 @@ public function getKeysArray(): Type public function getValuesArray(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray()); + $cb = static fn (Type $type): Type => $type->getValuesArray(); + if ($this->isList()->yes()) { + return $this; + } + return $this->intersectTypes($cb); } public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type @@ -1171,17 +1175,17 @@ public function flipArray(): Type public function intersectKeyArray(Type $otherArraysType): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType)); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType)); } public function popArray(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->popArray()); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->popArray()); } public function reverseArray(TrinaryLogic $preserveKeys): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); } public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type @@ -1191,23 +1195,21 @@ 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->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->shiftArray()); } 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 { - $result = $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); + $result = $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); if ( $this->isList()->yes() @@ -1223,7 +1225,7 @@ 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->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType)); } public function makeListMaybe(): Type @@ -1233,12 +1235,12 @@ public function makeListMaybe(): Type public function mapValueType(callable $cb): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->mapValueType($cb)); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->mapValueType($cb)); } public function mapKeyType(callable $cb): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->mapKeyType($cb)); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->mapKeyType($cb)); } public function makeAllArrayKeysOptional(): Type @@ -1248,12 +1250,12 @@ public function makeAllArrayKeysOptional(): Type public function changeKeyCaseArray(?int $case): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->changeKeyCaseArray($case)); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->changeKeyCaseArray($case)); } public function filterArrayRemovingFalsey(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->filterArrayRemovingFalsey()); + return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->filterArrayRemovingFalsey()); } public function getEnumCases(): array @@ -1714,6 +1716,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 = []; diff --git a/tests/PHPStan/Analyser/nsrt/bug-14633.php b/tests/PHPStan/Analyser/nsrt/bug-14633.php new file mode 100644 index 0000000000..6c2f3a6a8c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14633.php @@ -0,0 +1,180 @@ + 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); + } + + /** + * 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); + } + +} + +/** + * 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); + } + +}