@@ -715,7 +715,7 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T
715715 return $ builder ->getArray ();
716716 }
717717
718- public function unsetOffset (Type $ offsetType ): Type
718+ public function unsetOffset (Type $ offsetType, bool $ preserveListCertainty = false ): Type
719719 {
720720 $ offsetType = $ offsetType ->toArrayKey ();
721721 if ($ offsetType instanceof ConstantIntegerType || $ offsetType instanceof ConstantStringType) {
@@ -748,6 +748,7 @@ public function unsetOffset(Type $offsetType): Type
748748 $ newOptionalKeys ,
749749 $ this ->isList ,
750750 in_array ($ i , $ this ->optionalKeys , true ),
751+ $ preserveListCertainty ,
751752 );
752753
753754 return new self ($ newKeyTypes , $ newValueTypes , $ this ->nextAutoIndexes , $ newOptionalKeys , $ newIsList );
@@ -790,6 +791,7 @@ public function unsetOffset(Type $offsetType): Type
790791 $ optionalKeys ,
791792 $ this ->isList ,
792793 count ($ optionalKeys ) === count ($ this ->optionalKeys ),
794+ $ preserveListCertainty ,
793795 );
794796
795797 return new self ($ this ->keyTypes , $ this ->valueTypes , $ this ->nextAutoIndexes , $ optionalKeys , $ newIsList );
@@ -815,6 +817,7 @@ public function unsetOffset(Type $offsetType): Type
815817 $ optionalKeys ,
816818 $ this ->isList ,
817819 count ($ optionalKeys ) === count ($ this ->optionalKeys ),
820+ $ preserveListCertainty ,
818821 );
819822
820823 return new self ($ this ->keyTypes , $ this ->valueTypes , $ this ->nextAutoIndexes , $ optionalKeys , $ newIsList );
@@ -827,7 +830,7 @@ public function unsetOffset(Type $offsetType): Type
827830 * @param list<ConstantIntegerType|ConstantStringType> $newKeyTypes
828831 * @param int[] $newOptionalKeys
829832 */
830- private static function isListAfterUnset (array $ newKeyTypes , array $ newOptionalKeys , TrinaryLogic $ arrayIsList , bool $ unsetOptionalKey ): TrinaryLogic
833+ private static function isListAfterUnset (array $ newKeyTypes , array $ newOptionalKeys , TrinaryLogic $ arrayIsList , bool $ unsetOptionalKey, bool $ preserveListCertainty ): TrinaryLogic
831834 {
832835 if (!$ unsetOptionalKey || $ arrayIsList ->no ()) {
833836 return TrinaryLogic::createNo ();
@@ -851,7 +854,7 @@ private static function isListAfterUnset(array $newKeyTypes, array $newOptionalK
851854 }
852855 }
853856
854- return TrinaryLogic::createMaybe ();
857+ return $ preserveListCertainty ? $ arrayIsList : TrinaryLogic::createMaybe ();
855858 }
856859
857860 public function chunkArray (Type $ lengthType , TrinaryLogic $ preserveKeys ): Type
@@ -1531,7 +1534,9 @@ private function getKeysOrValuesArray(array $types): self
15311534
15321535 public function describe (VerbosityLevel $ level ): string
15331536 {
1534- $ describeValue = function (bool $ truncate ) use ($ level ): string {
1537+ $ arrayName = $ this ->shouldBeDescribedAsAList () ? 'list ' : 'array ' ;
1538+
1539+ $ describeValue = function (bool $ truncate ) use ($ level , $ arrayName ): string {
15351540 $ items = [];
15361541 $ values = [];
15371542 $ exportValuesOnly = true ;
@@ -1570,18 +1575,36 @@ public function describe(VerbosityLevel $level): string
15701575 }
15711576
15721577 return sprintf (
1573- 'array{%s%s} ' ,
1578+ '%s{%s%s} ' ,
1579+ $ arrayName ,
15741580 implode (', ' , $ exportValuesOnly ? $ values : $ items ),
15751581 $ append ,
15761582 );
15771583 };
15781584 return $ level ->handle (
1579- fn (): string => $ this ->isIterableAtLeastOnce ()->no () ? ' array ' : sprintf ('array <%s, %s> ' , $ this ->getIterableKeyType ()->describe ($ level ), $ this ->getIterableValueType ()->describe ($ level )),
1585+ fn (): string => $ this ->isIterableAtLeastOnce ()->no () ? $ arrayName : sprintf ('%s <%s, %s> ' , $ arrayName , $ this ->getIterableKeyType ()->describe ($ level ), $ this ->getIterableValueType ()->describe ($ level )),
15801586 static fn (): string => $ describeValue (true ),
15811587 static fn (): string => $ describeValue (false ),
15821588 );
15831589 }
15841590
1591+ private function shouldBeDescribedAsAList (): bool
1592+ {
1593+ if (!$ this ->isList ->yes ()) {
1594+ return false ;
1595+ }
1596+
1597+ if (count ($ this ->optionalKeys ) === 0 ) {
1598+ return false ;
1599+ }
1600+
1601+ if (count ($ this ->optionalKeys ) === 2 ) {
1602+ return true ;
1603+ }
1604+
1605+ return $ this ->optionalKeys [0 ] !== count ($ this ->keyTypes ) - 1 ;
1606+ }
1607+
15851608 public function inferTemplateTypes (Type $ receivedType ): TemplateTypeMap
15861609 {
15871610 if ($ receivedType instanceof UnionType || $ receivedType instanceof IntersectionType) {
@@ -1643,11 +1666,11 @@ public function tryRemove(Type $typeToRemove): ?Type
16431666 }
16441667
16451668 if ($ typeToRemove instanceof HasOffsetType) {
1646- return $ this ->unsetOffset ($ typeToRemove ->getOffsetType ());
1669+ return $ this ->unsetOffset ($ typeToRemove ->getOffsetType (), true );
16471670 }
16481671
16491672 if ($ typeToRemove instanceof HasOffsetValueType) {
1650- return $ this ->unsetOffset ($ typeToRemove ->getOffsetType ());
1673+ return $ this ->unsetOffset ($ typeToRemove ->getOffsetType (), true );
16511674 }
16521675
16531676 return null ;
@@ -1823,6 +1846,19 @@ public function makeOffsetRequired(Type $offsetType): self
18231846 return $ this ;
18241847 }
18251848
1849+ public function makeList (): Type
1850+ {
1851+ if ($ this ->isList ->yes ()) {
1852+ return $ this ;
1853+ }
1854+
1855+ if ($ this ->isList ->no ()) {
1856+ return new NeverType ();
1857+ }
1858+
1859+ return new self ($ this ->keyTypes , $ this ->valueTypes , $ this ->nextAutoIndexes , $ this ->optionalKeys , TrinaryLogic::createYes ());
1860+ }
1861+
18261862 public function toPhpDocNode (): TypeNode
18271863 {
18281864 $ items = [];
@@ -1863,7 +1899,10 @@ public function toPhpDocNode(): TypeNode
18631899 );
18641900 }
18651901
1866- return ArrayShapeNode::createSealed ($ exportValuesOnly ? $ values : $ items );
1902+ return ArrayShapeNode::createSealed (
1903+ $ exportValuesOnly ? $ values : $ items ,
1904+ $ this ->shouldBeDescribedAsAList () ? ArrayShapeNode::KIND_LIST : ArrayShapeNode::KIND_ARRAY ,
1905+ );
18671906 }
18681907
18691908 public static function isValidIdentifier (string $ value ): bool
0 commit comments