Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/Type/ArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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()),
);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
51 changes: 33 additions & 18 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -1151,7 +1151,11 @@

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
Expand All @@ -1171,17 +1175,17 @@

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
Expand All @@ -1191,23 +1195,21 @@

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()) {

Check warning on line 1204 in src/Type/IntersectionType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ public function shuffleArray(): Type { $cb = static fn (Type $type): Type => $type->shuffleArray(); - if ($this->isList()->yes()) { + if (!$this->isList()->no()) { return $this->intersectTypesPreserveTemplateType($cb); } return $this->intersectTypes($cb);

Check warning on line 1204 in src/Type/IntersectionType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ public function shuffleArray(): Type { $cb = static fn (Type $type): Type => $type->shuffleArray(); - if ($this->isList()->yes()) { + if (!$this->isList()->no()) { return $this->intersectTypesPreserveTemplateType($cb); } return $this->intersectTypes($cb);
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()
Expand All @@ -1223,7 +1225,7 @@

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
Expand All @@ -1233,12 +1235,12 @@

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
Expand All @@ -1248,12 +1250,12 @@

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
Expand Down Expand Up @@ -1714,6 +1716,19 @@
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 = [];
Expand Down
180 changes: 180 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14633.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php declare(strict_types=1);

namespace Bug14633;

use function PHPStan\Testing\assertType;

/**
* Tests for IntersectionType preserving TemplateType in array methods.
* Pattern: @template T with T&list<V> or T&array<K,V>
*/
class IntersectionTemplatePreservation
{

/**
* @template T
* @param T&list<int> $items
*/
public function popList(array $items): void
{
array_pop($items);
assertType('list<int>&T (method Bug14633\IntersectionTemplatePreservation::popList(), argument)', $items);
}

/**
* @template T
* @param T&array<string, int> $items
*/
public function popArray(array $items): void
{
array_pop($items);
assertType('array<string, int>&T (method Bug14633\IntersectionTemplatePreservation::popArray(), argument)', $items);
}

/**
* @template T
* @param T&list<int> $items
*/
public function shiftList(array $items): void
{
array_shift($items);
assertType('list<int>&T (method Bug14633\IntersectionTemplatePreservation::shiftList(), argument)', $items);
}

/**
* @template T
* @param T&array<string, int> $items
*/
public function shiftArray(array $items): void
{
array_shift($items);
assertType('array<string, int>&T (method Bug14633\IntersectionTemplatePreservation::shiftArray(), argument)', $items);
}

/**
* @template T
* @param T&list<int> $items
*/
public function reverseList(array $items): void
{
$reversed = array_reverse($items);
assertType('list<int>&T (method Bug14633\IntersectionTemplatePreservation::reverseList(), argument)', $reversed);
}

/**
* @template T
* @param T&array<string, int> $items
*/
public function reverseArrayPreserveKeys(array $items): void
{
$reversed = array_reverse($items, true);
assertType('array<string, int>&T (method Bug14633\IntersectionTemplatePreservation::reverseArrayPreserveKeys(), argument)', $reversed);
}

/**
* @template T
* @param T&list<int> $items
*/
public function sliceList(array $items): void
{
$sliced = array_slice($items, 1);
assertType('list<int>&T (method Bug14633\IntersectionTemplatePreservation::sliceList(), argument)', $sliced);
}

/**
* @template T
* @param T&array<string, int> $items
*/
public function sliceArrayPreserveKeys(array $items): void
{
$sliced = array_slice($items, 0, 5, true);
assertType('array<string, int>&T (method Bug14633\IntersectionTemplatePreservation::sliceArrayPreserveKeys(), argument)', $sliced);
}

/**
* @template T
* @param T&list<int> $items
*/
public function arrayValuesOnList(array $items): void
{
$values = array_values($items);
assertType('list<int>&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<string, int> $items
*/
public function arrayValuesOnNonList(array $items): void
{
$values = array_values($items);
assertType('list<int>', $values);
}

/**
* shuffle() on a list preserves T — keys are already sequential integers.
*
* @template T
* @param T&list<int> $items
*/
public function shuffleOnList(array $items): void
{
shuffle($items);
assertType('list<int>&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<string, int> $items
*/
public function shuffleOnNonList(array $items): void
{
shuffle($items);
assertType('list<int>', $items);
}

}

/**
* Tests for ArrayType methods preserving template via $this->withTypes().
* Pattern: @template T of array<K,V>
*/
class ArrayTypeTemplatePreservation
{

/**
* @template T of array<string, int>
* @param T $items
*/
public function filterArrayRemovingFalsey(array $items): void
{
$result = array_filter($items);
assertType('T of array<string, int<min, -1>|int<1, max>> (method Bug14633\ArrayTypeTemplatePreservation::filterArrayRemovingFalsey(), argument)', $result);
}

/**
* @template T of array<string, int>
* @param T $items
* @param array<string, mixed> $other
*/
public function intersectKeyArray(array $items, array $other): void
{
$result = array_intersect_key($items, $other);
assertType('T of array<string, int> (method Bug14633\ArrayTypeTemplatePreservation::intersectKeyArray(), argument)', $result);
}

/**
* @template T of array<int, int>
* @param T $items
*/
public function sliceArray(array $items): void
{
$result = array_slice($items, 1);
assertType('T of array<int<0, max>, int> (method Bug14633\ArrayTypeTemplatePreservation::sliceArray(), argument)&list', $result);
}

}
Loading