4545use PHPStan \Type \Traits \NonGeneralizableTypeTrait ;
4646use PHPStan \Type \Type ;
4747use PHPStan \Type \TypeCombinator ;
48- use PHPStan \Type \TypeTraverser ;
4948use PHPStan \Type \TypeUtils ;
5049use PHPStan \Type \UnionType ;
5150use PHPStan \Type \VerbosityLevel ;
51+ use SplObjectStorage ;
5252use function array_fill_keys ;
5353use function is_callable ;
5454use function sprintf ;
@@ -133,13 +133,23 @@ public function describe(VerbosityLevel $level): string
133133
134134 public function isResolvable (): bool
135135 {
136+ $ hasTemplateType = false ;
137+
136138 foreach ($ this ->types as $ type ) {
137- if (TypeUtils::containsTemplateType ($ type )) {
138- return false ;
139+ $ type = self ::removeTopLevelNeverAlternatives ($ type );
140+
141+ if (!TypeUtils::containsTemplateType ($ type )) {
142+ continue ;
143+ }
144+
145+ if (!$ type ->isArray ()->yes ()) {
146+ return true ;
139147 }
148+
149+ $ hasTemplateType = true ;
140150 }
141151
142- return true ;
152+ return ! $ hasTemplateType ;
143153 }
144154
145155 protected function getResult (): Type
@@ -150,7 +160,7 @@ protected function getResult(): Type
150160 $ types = [];
151161
152162 foreach ($ this ->types as $ type ) {
153- $ type = self ::removeUninhabitedConstantArrays ($ type );
163+ $ type = self ::normalizeUninhabitedArraysForInference ($ type );
154164 $ type = self ::removeTopLevelNeverAlternatives ($ type );
155165
156166 if ($ type instanceof NeverType) {
@@ -366,69 +376,138 @@ private static function normalizeArrayMergeKeyType(Type $keyType): Type
366376 return $ keyType instanceof MixedType ? $ keyType : $ keyType ->toArrayKey ();
367377 }
368378
369- private static function removeUninhabitedConstantArrays (Type $ type ): Type
379+ /** @internal */
380+ public static function normalizeUninhabitedArrays (Type $ type ): Type
370381 {
371- return TypeTraverser::map ($ type , static function (Type $ innerType , callable $ traverse ): Type {
372- $ innerType = $ traverse ($ innerType );
373-
374- if ($ innerType instanceof ConstantArrayType) {
375- $ hasOptionalNever = false ;
376- $ hasIntegerKey = false ;
377- $ keyTypes = [];
378- $ valueTypes = [];
379- $ optionalKeys = [];
380- $ optionalKeyLookup = array_fill_keys ($ innerType ->getOptionalKeys (), true );
381-
382- foreach ($ innerType ->getKeyTypes () as $ i => $ keyType ) {
383- $ valueType = $ innerType ->getValueTypes ()[$ i ];
384- $ hasIntegerKey = $ hasIntegerKey || $ keyType instanceof ConstantIntegerType;
385- $ isOptionalKey = isset ($ optionalKeyLookup [$ i ]);
386-
387- if ($ valueType instanceof NeverType) {
388- if (!$ isOptionalKey ) {
389- return new NeverType ();
390- }
382+ /** @var SplObjectStorage<Type, Type> $normalizedTypes */
383+ $ normalizedTypes = new SplObjectStorage ();
391384
392- $ hasOptionalNever = true ;
393- continue ;
394- }
385+ return self ::normalizeUninhabitedArraysWithMemo ($ type , $ normalizedTypes , false );
386+ }
395387
396- if ($ isOptionalKey ) {
397- $ optionalKeys [] = count ($ keyTypes );
398- }
388+ private static function normalizeUninhabitedArraysForInference (Type $ type ): Type
389+ {
390+ /** @var SplObjectStorage<Type, Type> $normalizedTypes */
391+ $ normalizedTypes = new SplObjectStorage ();
399392
400- $ keyTypes [] = $ keyType ;
401- $ valueTypes [] = $ valueType ;
402- }
393+ return self ::normalizeUninhabitedArraysWithMemo ($ type , $ normalizedTypes , true );
394+ }
403395
404- if (!$ hasOptionalNever ) {
405- return $ innerType ;
406- }
396+ /**
397+ * @param SplObjectStorage<Type, Type> $normalizedTypes
398+ */
399+ private static function normalizeUninhabitedArraysWithMemo (
400+ Type $ type ,
401+ SplObjectStorage $ normalizedTypes ,
402+ bool $ useTemplateBounds ,
403+ ): Type {
404+ if (isset ($ normalizedTypes [$ type ])) {
405+ return $ normalizedTypes [$ type ];
406+ }
407407
408- if (self ::hasUnknownExtraOffsets ($ innerType )) {
409- return $ innerType ;
410- }
408+ // Break cycles conservatively while this node is being normalized.
409+ $ normalizedTypes [$ type ] = $ type ;
411410
412- if ([0 ] !== $ innerType ->getNextAutoIndexes ()) {
413- return $ innerType ;
414- }
411+ if ($ type instanceof TemplateType) {
412+ $ bound = self ::normalizeUninhabitedArraysWithMemo (
413+ $ type ->getBound (),
414+ $ normalizedTypes ,
415+ $ useTemplateBounds ,
416+ );
417+ $ result = $ bound instanceof NeverType || $ useTemplateBounds ? $ bound : $ type ;
418+ $ normalizedTypes [$ type ] = $ result ;
419+
420+ return $ result ;
421+ }
415422
416- if ($ hasIntegerKey ) {
417- return $ innerType ;
423+ $ traversedType = $ type ->traverse (
424+ static fn (Type $ innerType ): Type =>
425+ self ::normalizeUninhabitedArraysWithMemo (
426+ $ innerType ,
427+ $ normalizedTypes ,
428+ $ useTemplateBounds ,
429+ ),
430+ );
431+ $ result = self ::normalizeTraversedUninhabitedArray ($ traversedType );
432+ $ normalizedTypes [$ type ] = $ result ;
433+
434+ return $ result ;
435+ }
436+
437+ private static function normalizeTraversedUninhabitedArray (Type $ type ): Type
438+ {
439+ if (
440+ $ type ->isArray ()->yes ()
441+ && $ type ->isIterableAtLeastOnce ()->yes ()
442+ && (
443+ $ type ->getIterableKeyType () instanceof NeverType
444+ || $ type ->getIterableValueType () instanceof NeverType
445+ )
446+ ) {
447+ return new NeverType ();
448+ }
449+
450+ $ constantArrays = $ type ->getConstantArrays ();
451+
452+ if (count ($ constantArrays ) !== 1 || $ type !== $ constantArrays [0 ]) {
453+ return $ type ;
454+ }
455+
456+ $ type = $ constantArrays [0 ];
457+
458+ $ hasOptionalNever = false ;
459+ $ hasIntegerKey = false ;
460+ $ keyTypes = [];
461+ $ valueTypes = [];
462+ $ optionalKeys = [];
463+ $ optionalKeyLookup = array_fill_keys ($ type ->getOptionalKeys (), true );
464+
465+ foreach ($ type ->getKeyTypes () as $ i => $ keyType ) {
466+ $ valueType = $ type ->getValueTypes ()[$ i ];
467+ $ hasIntegerKey = $ hasIntegerKey || $ keyType instanceof ConstantIntegerType;
468+ $ isOptionalKey = isset ($ optionalKeyLookup [$ i ]);
469+
470+ if ($ valueType instanceof NeverType) {
471+ if (!$ isOptionalKey ) {
472+ return new NeverType ();
418473 }
419474
420- return new ConstantArrayType (
421- $ keyTypes ,
422- $ valueTypes ,
423- $ innerType ->getNextAutoIndexes (),
424- $ optionalKeys ,
425- [] === $ keyTypes ? TrinaryLogic::createYes () : $ innerType ->isList (),
426- self ::getUnsealedTypes ($ innerType ),
427- );
475+ $ hasOptionalNever = true ;
476+ continue ;
477+ }
478+
479+ if ($ isOptionalKey ) {
480+ $ optionalKeys [] = count ($ keyTypes );
428481 }
429482
430- return $ innerType ;
431- });
483+ $ keyTypes [] = $ keyType ;
484+ $ valueTypes [] = $ valueType ;
485+ }
486+
487+ if (!$ hasOptionalNever ) {
488+ return $ type ;
489+ }
490+
491+ if (self ::hasUnknownExtraOffsets ($ type )) {
492+ return $ type ;
493+ }
494+
495+ if ([0 ] !== $ type ->getNextAutoIndexes ()) {
496+ return $ type ;
497+ }
498+
499+ if ($ hasIntegerKey ) {
500+ return $ type ;
501+ }
502+
503+ return new ConstantArrayType (
504+ $ keyTypes ,
505+ $ valueTypes ,
506+ $ type ->getNextAutoIndexes (),
507+ $ optionalKeys ,
508+ [] === $ keyTypes ? TrinaryLogic::createYes () : $ type ->isList (),
509+ self ::getUnsealedTypes ($ type ),
510+ );
432511 }
433512
434513 private static function getOptionalMethod (object $ object , string $ method ): ?Closure
0 commit comments