33namespace PHPStan \Type ;
44
55use PHPStan \Php \PhpVersion ;
6+ use PHPStan \PhpDocParser \Ast \Type \ArrayShapeNode ;
67use PHPStan \PhpDocParser \Ast \Type \GenericTypeNode ;
78use PHPStan \PhpDocParser \Ast \Type \IdentifierTypeNode ;
89use PHPStan \PhpDocParser \Ast \Type \IntersectionTypeNode ;
4546use function ksort ;
4647use function md5 ;
4748use function sprintf ;
48- use function str_starts_with ;
4949use function strcasecmp ;
5050use function strlen ;
5151use function substr ;
@@ -347,6 +347,10 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)
347347
348348 $ nonEmptyStr = false ;
349349 $ nonFalsyStr = false ;
350+ $ isList = $ this ->isList ()->yes ();
351+ $ isArray = $ this ->isArray ()->yes ();
352+ $ isNonEmptyArray = $ this ->isIterableAtLeastOnce ()->yes ();
353+ $ describedTypes = [];
350354 foreach ($ this ->getSortedTypes () as $ i => $ type ) {
351355 if ($ type instanceof AccessoryNonEmptyStringType
352356 || $ type instanceof AccessoryLiteralStringType
@@ -379,10 +383,45 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)
379383 $ skipTypeNames [] = 'string ' ;
380384 continue ;
381385 }
382- if ($ type instanceof NonEmptyArrayType || $ type instanceof AccessoryArrayListType) {
383- $ typesToDescribe [$ i ] = $ type ;
384- $ skipTypeNames [] = 'array ' ;
385- continue ;
386+ if ($ isList || $ isArray ) {
387+ if ($ type instanceof ArrayType) {
388+ $ keyType = $ type ->getKeyType ();
389+ $ valueType = $ type ->getItemType ();
390+ if ($ isList ) {
391+ $ isMixedValueType = $ valueType instanceof MixedType && $ valueType ->describe (VerbosityLevel::precise ()) === 'mixed ' && !$ valueType ->isExplicitMixed ();
392+ $ valueTypeDescription = '' ;
393+ if (!$ isMixedValueType ) {
394+ $ valueTypeDescription = sprintf ('<%s> ' , $ valueType ->describe ($ level ));
395+ }
396+
397+ $ describedTypes [$ i ] = ($ isNonEmptyArray ? 'non-empty-list ' : 'list ' ) . $ valueTypeDescription ;
398+ } else {
399+ $ isMixedKeyType = $ keyType instanceof MixedType && $ keyType ->describe (VerbosityLevel::precise ()) === 'mixed ' && !$ keyType ->isExplicitMixed ();
400+ $ isMixedValueType = $ valueType instanceof MixedType && $ valueType ->describe (VerbosityLevel::precise ()) === 'mixed ' && !$ valueType ->isExplicitMixed ();
401+ $ typeDescription = '' ;
402+ if (!$ isMixedKeyType ) {
403+ $ typeDescription = sprintf ('<%s, %s> ' , $ keyType ->describe ($ level ), $ valueType ->describe ($ level ));
404+ } elseif (!$ isMixedValueType ) {
405+ $ typeDescription = sprintf ('<%s> ' , $ valueType ->describe ($ level ));
406+ }
407+
408+ $ describedTypes [$ i ] = ($ isNonEmptyArray ? 'non-empty-array ' : 'array ' ) . $ typeDescription ;
409+ }
410+ continue ;
411+ } elseif ($ type instanceof ConstantArrayType) {
412+ $ description = $ type ->describe ($ level );
413+ $ descriptionWithoutKind = substr ($ description , strlen ('array ' ));
414+ $ begin = $ isList ? 'list ' : 'array ' ;
415+ if ($ isNonEmptyArray && !$ type ->isIterableAtLeastOnce ()->yes ()) {
416+ $ begin = 'non-empty- ' . $ begin ;
417+ }
418+
419+ $ describedTypes [$ i ] = $ begin . $ descriptionWithoutKind ;
420+ continue ;
421+ }
422+ if ($ type instanceof NonEmptyArrayType || $ type instanceof AccessoryArrayListType) {
423+ continue ;
424+ }
386425 }
387426
388427 if ($ type instanceof CallableType && $ type ->isCommonCallable ()) {
@@ -404,7 +443,6 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)
404443 $ typesToDescribe [$ i ] = $ type ;
405444 }
406445
407- $ describedTypes = [];
408446 foreach ($ baseTypes as $ i => $ type ) {
409447 $ typeDescription = $ type ->describe ($ level );
410448
@@ -418,36 +456,6 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)
418456 }
419457 }
420458
421- if (
422- str_starts_with ($ typeDescription , 'array< ' )
423- && in_array ('array ' , $ skipTypeNames , true )
424- ) {
425- $ nonEmpty = false ;
426- $ typeName = 'array ' ;
427- foreach ($ typesToDescribe as $ j => $ typeToDescribe ) {
428- if (
429- $ typeToDescribe instanceof AccessoryArrayListType
430- && substr ($ typeDescription , 0 , strlen ('array<int<0, max>, ' )) === 'array<int<0, max>, '
431- ) {
432- $ typeName = 'list ' ;
433- $ typeDescription = 'array< ' . substr ($ typeDescription , strlen ('array<int<0, max>, ' ));
434- } elseif ($ typeToDescribe instanceof NonEmptyArrayType) {
435- $ nonEmpty = true ;
436- } else {
437- continue ;
438- }
439-
440- unset($ typesToDescribe [$ j ]);
441- }
442-
443- if ($ nonEmpty ) {
444- $ typeName = 'non-empty- ' . $ typeName ;
445- }
446-
447- $ describedTypes [$ i ] = $ typeName . '< ' . substr ($ typeDescription , strlen ('array< ' ));
448- continue ;
449- }
450-
451459 if (in_array ($ typeDescription , $ skipTypeNames , true )) {
452460 continue ;
453461 }
@@ -1139,6 +1147,10 @@ public function toPhpDocNode(): TypeNode
11391147
11401148 $ nonEmptyStr = false ;
11411149 $ nonFalsyStr = false ;
1150+ $ isList = $ this ->isList ()->yes ();
1151+ $ isArray = $ this ->isArray ()->yes ();
1152+ $ isNonEmptyArray = $ this ->isIterableAtLeastOnce ()->yes ();
1153+ $ describedTypes = [];
11421154
11431155 foreach ($ this ->getSortedTypes () as $ i => $ type ) {
11441156 if ($ type instanceof AccessoryNonEmptyStringType
@@ -1168,11 +1180,70 @@ public function toPhpDocNode(): TypeNode
11681180 $ skipTypeNames [] = 'string ' ;
11691181 continue ;
11701182 }
1171- if ($ type instanceof NonEmptyArrayType || $ type instanceof AccessoryArrayListType) {
1172- $ typesToDescribe [$ i ] = $ type ;
1173- $ skipTypeNames [] = 'array ' ;
1174- continue ;
1183+
1184+ if ($ isList || $ isArray ) {
1185+ if ($ type instanceof ArrayType) {
1186+ $ keyType = $ type ->getKeyType ();
1187+ $ valueType = $ type ->getItemType ();
1188+ if ($ isList ) {
1189+ $ isMixedValueType = $ valueType instanceof MixedType && $ valueType ->describe (VerbosityLevel::precise ()) === 'mixed ' && !$ valueType ->isExplicitMixed ();
1190+ $ identifierTypeNode = new IdentifierTypeNode ($ isNonEmptyArray ? 'non-empty-list ' : 'list ' );
1191+ if (!$ isMixedValueType ) {
1192+ $ describedTypes [$ i ] = new GenericTypeNode ($ identifierTypeNode , [
1193+ $ valueType ->toPhpDocNode (),
1194+ ]);
1195+ } else {
1196+ $ describedTypes [$ i ] = $ identifierTypeNode ;
1197+ }
1198+ } else {
1199+ $ isMixedKeyType = $ keyType instanceof MixedType && $ keyType ->describe (VerbosityLevel::precise ()) === 'mixed ' && !$ keyType ->isExplicitMixed ();
1200+ $ isMixedValueType = $ valueType instanceof MixedType && $ valueType ->describe (VerbosityLevel::precise ()) === 'mixed ' && !$ valueType ->isExplicitMixed ();
1201+ $ identifierTypeNode = new IdentifierTypeNode ($ isNonEmptyArray ? 'non-empty-array ' : 'array ' );
1202+ if (!$ isMixedKeyType ) {
1203+ $ describedTypes [$ i ] = new GenericTypeNode ($ identifierTypeNode , [
1204+ $ keyType ->toPhpDocNode (),
1205+ $ valueType ->toPhpDocNode (),
1206+ ]);
1207+ } elseif (!$ isMixedValueType ) {
1208+ $ describedTypes [$ i ] = new GenericTypeNode ($ identifierTypeNode , [
1209+ $ valueType ->toPhpDocNode (),
1210+ ]);
1211+ } else {
1212+ $ describedTypes [$ i ] = $ identifierTypeNode ;
1213+ }
1214+ }
1215+ continue ;
1216+ } elseif ($ type instanceof ConstantArrayType) {
1217+ $ constantArrayTypeNode = $ type ->toPhpDocNode ();
1218+ if ($ constantArrayTypeNode instanceof ArrayShapeNode) {
1219+ $ newKind = $ constantArrayTypeNode ->kind ;
1220+ if ($ isList ) {
1221+ if ($ isNonEmptyArray && !$ type ->isIterableAtLeastOnce ()->yes ()) {
1222+ $ newKind = ArrayShapeNode::KIND_NON_EMPTY_LIST ;
1223+ } else {
1224+ $ newKind = ArrayShapeNode::KIND_LIST ;
1225+ }
1226+ } elseif ($ isNonEmptyArray && !$ type ->isIterableAtLeastOnce ()->yes ()) {
1227+ $ newKind = ArrayShapeNode::KIND_NON_EMPTY_ARRAY ;
1228+ }
1229+
1230+ if ($ newKind !== $ constantArrayTypeNode ->kind ) {
1231+ if ($ constantArrayTypeNode ->sealed ) {
1232+ $ constantArrayTypeNode = ArrayShapeNode::createSealed ($ constantArrayTypeNode ->items , $ newKind );
1233+ } else {
1234+ $ constantArrayTypeNode = ArrayShapeNode::createUnsealed ($ constantArrayTypeNode ->items , $ constantArrayTypeNode ->unsealedType , $ newKind );
1235+ }
1236+ }
1237+
1238+ $ describedTypes [$ i ] = $ constantArrayTypeNode ;
1239+ continue ;
1240+ }
1241+ }
1242+ if ($ type instanceof NonEmptyArrayType || $ type instanceof AccessoryArrayListType) {
1243+ continue ;
1244+ }
11751245 }
1246+
11761247 if (!$ type instanceof AccessoryType) {
11771248 $ baseTypes [$ i ] = $ type ;
11781249 continue ;
@@ -1186,7 +1257,6 @@ public function toPhpDocNode(): TypeNode
11861257 $ typesToDescribe [$ i ] = $ type ;
11871258 }
11881259
1189- $ describedTypes = [];
11901260 foreach ($ baseTypes as $ i => $ type ) {
11911261 $ typeNode = $ type ->toPhpDocNode ();
11921262 if ($ typeNode instanceof GenericTypeNode && $ typeNode ->type ->name === 'array ' ) {
0 commit comments