1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Php\PhpVersion;
6: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
7: use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
8: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
9: use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
10: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
11: use PHPStan\Reflection\ClassConstantReflection;
12: use PHPStan\Reflection\ClassMemberAccessAnswerer;
13: use PHPStan\Reflection\ExtendedMethodReflection;
14: use PHPStan\Reflection\ExtendedPropertyReflection;
15: use PHPStan\Reflection\InitializerExprTypeResolver;
16: use PHPStan\Reflection\MissingConstantFromReflectionException;
17: use PHPStan\Reflection\MissingMethodFromReflectionException;
18: use PHPStan\Reflection\MissingPropertyFromReflectionException;
19: use PHPStan\Reflection\TrivialParametersAcceptor;
20: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection;
21: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection;
22: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
23: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
24: use PHPStan\ShouldNotHappenException;
25: use PHPStan\TrinaryLogic;
26: use PHPStan\Type\Accessory\AccessoryArrayListType;
27: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
28: use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
29: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
30: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
31: use PHPStan\Type\Accessory\AccessoryNumericStringType;
32: use PHPStan\Type\Accessory\AccessoryType;
33: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
34: use PHPStan\Type\Accessory\HasOffsetType;
35: use PHPStan\Type\Accessory\HasOffsetValueType;
36: use PHPStan\Type\Accessory\NonEmptyArrayType;
37: use PHPStan\Type\Constant\ConstantArrayType;
38: use PHPStan\Type\Constant\ConstantIntegerType;
39: use PHPStan\Type\Constant\ConstantStringType;
40: use PHPStan\Type\Generic\TemplateType;
41: use PHPStan\Type\Generic\TemplateTypeMap;
42: use PHPStan\Type\Generic\TemplateTypeVariance;
43: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
44: use PHPStan\Type\Traits\NonRemoveableTypeTrait;
45: use function array_filter;
46: use function array_intersect_key;
47: use function array_map;
48: use function array_shift;
49: use function array_unique;
50: use function array_values;
51: use function count;
52: use function implode;
53: use function in_array;
54: use function is_int;
55: use function ksort;
56: use function md5;
57: use function sprintf;
58: use function strcasecmp;
59: use function strlen;
60: use function substr;
61: use function usort;
62:
63: /** @api */
64: class IntersectionType implements CompoundType
65: {
66:
67: use NonRemoveableTypeTrait;
68: use NonGeneralizableTypeTrait;
69:
70: private bool $sortedTypes = false;
71:
72: /**
73: * @api
74: * @param Type[] $types
75: */
76: public function __construct(private array $types)
77: {
78: if (count($types) < 2) {
79: throw new ShouldNotHappenException(sprintf(
80: 'Cannot create %s with: %s',
81: self::class,
82: implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)),
83: ));
84: }
85: }
86:
87: /**
88: * @return Type[]
89: */
90: public function getTypes(): array
91: {
92: return $this->types;
93: }
94:
95: /**
96: * @return Type[]
97: */
98: private function getSortedTypes(): array
99: {
100: if ($this->sortedTypes) {
101: return $this->types;
102: }
103:
104: $this->types = UnionTypeHelper::sortTypes($this->types);
105: $this->sortedTypes = true;
106:
107: return $this->types;
108: }
109:
110: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
111: {
112: $types = TemplateTypeMap::createEmpty();
113:
114: foreach ($this->types as $type) {
115: $types = $types->intersect($templateType->inferTemplateTypes($type));
116: }
117:
118: return $types;
119: }
120:
121: public function getReferencedClasses(): array
122: {
123: $classes = [];
124: foreach ($this->types as $type) {
125: foreach ($type->getReferencedClasses() as $className) {
126: $classes[] = $className;
127: }
128: }
129:
130: return $classes;
131: }
132:
133: public function getObjectClassNames(): array
134: {
135: $objectClassNames = [];
136: foreach ($this->types as $type) {
137: $innerObjectClassNames = $type->getObjectClassNames();
138: foreach ($innerObjectClassNames as $innerObjectClassName) {
139: $objectClassNames[] = $innerObjectClassName;
140: }
141: }
142:
143: return array_values(array_unique($objectClassNames));
144: }
145:
146: public function getObjectClassReflections(): array
147: {
148: $reflections = [];
149: foreach ($this->types as $type) {
150: foreach ($type->getObjectClassReflections() as $reflection) {
151: $reflections[] = $reflection;
152: }
153: }
154:
155: return $reflections;
156: }
157:
158: public function getArrays(): array
159: {
160: $arrays = [];
161: foreach ($this->types as $type) {
162: foreach ($type->getArrays() as $array) {
163: $arrays[] = $array;
164: }
165: }
166:
167: return $arrays;
168: }
169:
170: public function getConstantArrays(): array
171: {
172: $constantArrays = [];
173: foreach ($this->types as $type) {
174: foreach ($type->getConstantArrays() as $constantArray) {
175: $constantArrays[] = $constantArray;
176: }
177: }
178:
179: return $constantArrays;
180: }
181:
182: public function getConstantStrings(): array
183: {
184: $strings = [];
185: foreach ($this->types as $type) {
186: foreach ($type->getConstantStrings() as $string) {
187: $strings[] = $string;
188: }
189: }
190:
191: return $strings;
192: }
193:
194: public function accepts(Type $otherType, bool $strictTypes): AcceptsResult
195: {
196: $result = AcceptsResult::createYes();
197: foreach ($this->types as $type) {
198: $result = $result->and($type->accepts($otherType, $strictTypes));
199: }
200:
201: if (!$result->yes()) {
202: $isList = $otherType->isList();
203: $reasons = $result->reasons;
204: $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $otherType);
205: if ($this->isList()->yes() && !$isList->yes()) {
206: $reasons[] = sprintf(
207: '%s %s a list.',
208: $otherType->describe($verbosity),
209: $isList->no() ? 'is not' : 'might not be',
210: );
211: }
212:
213: $isNonEmpty = $otherType->isIterableAtLeastOnce();
214: if ($this->isIterableAtLeastOnce()->yes() && !$isNonEmpty->yes()) {
215: $reasons[] = sprintf(
216: '%s %s empty.',
217: $otherType->describe($verbosity),
218: $isNonEmpty->no() ? 'is' : 'might be',
219: );
220: }
221:
222: if (count($reasons) > 0) {
223: return new AcceptsResult($result->result, $reasons);
224: }
225: }
226:
227: return $result;
228: }
229:
230: public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult
231: {
232: if ($otherType instanceof IntersectionType && $this->equals($otherType)) {
233: return IsSuperTypeOfResult::createYes();
234: }
235:
236: if ($otherType instanceof NeverType) {
237: return IsSuperTypeOfResult::createYes();
238: }
239:
240: return IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types));
241: }
242:
243: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
244: {
245: if (($otherType instanceof self || $otherType instanceof UnionType) && !$otherType instanceof TemplateType) {
246: return $otherType->isSuperTypeOf($this);
247: }
248:
249: $result = IsSuperTypeOfResult::maxMin(...array_map(static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType), $this->types));
250: if (
251: !$result->no()
252: && $this->isOversizedArray()->yes()
253: && !$otherType->isIterableAtLeastOnce()->no()
254: ) {
255: return IsSuperTypeOfResult::createYes();
256: }
257:
258: return $result;
259: }
260:
261: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
262: {
263: $result = AcceptsResult::maxMin(...array_map(static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), $this->types));
264: if ($this->isOversizedArray()->yes()) {
265: if (!$result->no()) {
266: return AcceptsResult::createYes();
267: }
268: }
269:
270: return $result;
271: }
272:
273: public function equals(Type $type): bool
274: {
275: if (!$type instanceof static) {
276: return false;
277: }
278:
279: if (count($this->types) !== count($type->types)) {
280: return false;
281: }
282:
283: $otherTypes = $type->types;
284: foreach ($this->types as $innerType) {
285: $match = false;
286: foreach ($otherTypes as $i => $otherType) {
287: if (!$innerType->equals($otherType)) {
288: continue;
289: }
290:
291: $match = true;
292: unset($otherTypes[$i]);
293: break;
294: }
295:
296: if (!$match) {
297: return false;
298: }
299: }
300:
301: return count($otherTypes) === 0;
302: }
303:
304: public function describe(VerbosityLevel $level): string
305: {
306: return $level->handle(
307: function () use ($level): string {
308: $typeNames = [];
309: $isList = $this->isList()->yes();
310: $valueType = null;
311: foreach ($this->getSortedTypes() as $type) {
312: if ($isList) {
313: if ($type instanceof ArrayType || $type instanceof ConstantArrayType) {
314: $valueType = $type->getIterableValueType();
315: continue;
316: }
317: if ($type instanceof NonEmptyArrayType) {
318: continue;
319: }
320: }
321: if ($type instanceof AccessoryType) {
322: continue;
323: }
324: $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level);
325: }
326:
327: if ($isList) {
328: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
329: $innerType = '';
330: if ($valueType !== null && !$isMixedValueType) {
331: $innerType = sprintf('<%s>', $valueType->describe($level));
332: }
333:
334: $typeNames[] = 'list' . $innerType;
335: }
336:
337: usort($typeNames, static function ($a, $b) {
338: $cmp = strcasecmp($a, $b);
339: if ($cmp !== 0) {
340: return $cmp;
341: }
342:
343: return $a <=> $b;
344: });
345:
346: return implode('&', $typeNames);
347: },
348: fn (): string => $this->describeItself($level, true),
349: fn (): string => $this->describeItself($level, false),
350: );
351: }
352:
353: private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string
354: {
355: $baseTypes = [];
356: $typesToDescribe = [];
357: $skipTypeNames = [];
358:
359: $nonEmptyStr = false;
360: $nonFalsyStr = false;
361: $isList = $this->isList()->yes();
362: $isArray = $this->isArray()->yes();
363: $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
364: $describedTypes = [];
365: foreach ($this->getSortedTypes() as $i => $type) {
366: if ($type instanceof AccessoryNonEmptyStringType
367: || $type instanceof AccessoryLiteralStringType
368: || $type instanceof AccessoryNumericStringType
369: || $type instanceof AccessoryNonFalsyStringType
370: || $type instanceof AccessoryLowercaseStringType
371: || $type instanceof AccessoryUppercaseStringType
372: ) {
373: if (
374: ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType)
375: && !$level->isPrecise()
376: && !$level->isCache()
377: ) {
378: continue;
379: }
380: if ($type instanceof AccessoryNonFalsyStringType) {
381: $nonFalsyStr = true;
382: }
383: if ($type instanceof AccessoryNonEmptyStringType) {
384: $nonEmptyStr = true;
385: }
386: if ($nonEmptyStr && $nonFalsyStr) {
387: // prevent redundant 'non-empty-string&non-falsy-string'
388: foreach ($typesToDescribe as $key => $typeToDescribe) {
389: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
390: continue;
391: }
392:
393: unset($typesToDescribe[$key]);
394: }
395: }
396:
397: $typesToDescribe[$i] = $type;
398: $skipTypeNames[] = 'string';
399: continue;
400: }
401: if ($isList || $isArray) {
402: if ($type instanceof ArrayType) {
403: $keyType = $type->getKeyType();
404: $valueType = $type->getItemType();
405: if ($isList) {
406: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
407: $valueTypeDescription = '';
408: if (!$isMixedValueType) {
409: $valueTypeDescription = sprintf('<%s>', $valueType->describe($level));
410: }
411:
412: $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-list' : 'list') . $valueTypeDescription;
413: } else {
414: $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
415: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
416: $typeDescription = '';
417: if (!$isMixedKeyType) {
418: $typeDescription = sprintf('<%s, %s>', $keyType->describe($level), $valueType->describe($level));
419: } elseif (!$isMixedValueType) {
420: $typeDescription = sprintf('<%s>', $valueType->describe($level));
421: }
422:
423: $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-array' : 'array') . $typeDescription;
424: }
425: continue;
426: } elseif ($type instanceof ConstantArrayType) {
427: $description = $type->describe($level);
428: $descriptionWithoutKind = substr($description, strlen('array'));
429: $begin = $isList ? 'list' : 'array';
430: if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
431: $begin = 'non-empty-' . $begin;
432: }
433:
434: $describedTypes[$i] = $begin . $descriptionWithoutKind;
435: continue;
436: }
437: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
438: continue;
439: }
440: }
441:
442: if ($type instanceof CallableType && $type->isCommonCallable()) {
443: $typesToDescribe[$i] = $type;
444: $skipTypeNames[] = 'object';
445: $skipTypeNames[] = 'string';
446: continue;
447: }
448:
449: if (!$type instanceof AccessoryType) {
450: $baseTypes[$i] = $type;
451: continue;
452: }
453:
454: if ($skipAccessoryTypes) {
455: continue;
456: }
457:
458: $typesToDescribe[$i] = $type;
459: }
460:
461: foreach ($baseTypes as $i => $type) {
462: $typeDescription = $type->describe($level);
463:
464: if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) {
465: foreach ($typesToDescribe as $j => $typeToDescribe) {
466: if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) {
467: $describedTypes[$i] = 'callable-' . $typeDescription;
468: unset($typesToDescribe[$j]);
469: continue 2;
470: }
471: }
472: }
473:
474: if (in_array($typeDescription, $skipTypeNames, true)) {
475: continue;
476: }
477:
478: $describedTypes[$i] = $type->describe($level);
479: }
480:
481: foreach ($typesToDescribe as $i => $typeToDescribe) {
482: $describedTypes[$i] = $typeToDescribe->describe($level);
483: }
484:
485: ksort($describedTypes);
486:
487: return implode('&', $describedTypes);
488: }
489:
490: public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type
491: {
492: return $this->intersectTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName));
493: }
494:
495: public function isObject(): TrinaryLogic
496: {
497: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isObject());
498: }
499:
500: public function isEnum(): TrinaryLogic
501: {
502: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isEnum());
503: }
504:
505: public function canAccessProperties(): TrinaryLogic
506: {
507: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
508: }
509:
510: public function hasProperty(string $propertyName): TrinaryLogic
511: {
512: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
513: }
514:
515: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
516: {
517: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
518: }
519:
520: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
521: {
522: $propertyPrototypes = [];
523: foreach ($this->types as $type) {
524: if (!$type->hasProperty($propertyName)->yes()) {
525: continue;
526: }
527:
528: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
529: }
530:
531: $propertiesCount = count($propertyPrototypes);
532: if ($propertiesCount === 0) {
533: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
534: }
535:
536: if ($propertiesCount === 1) {
537: return $propertyPrototypes[0];
538: }
539:
540: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes);
541: }
542:
543: public function hasInstanceProperty(string $propertyName): TrinaryLogic
544: {
545: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName));
546: }
547:
548: public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
549: {
550: return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty();
551: }
552:
553: public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
554: {
555: $propertyPrototypes = [];
556: foreach ($this->types as $type) {
557: if (!$type->hasInstanceProperty($propertyName)->yes()) {
558: continue;
559: }
560:
561: $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this);
562: }
563:
564: $propertiesCount = count($propertyPrototypes);
565: if ($propertiesCount === 0) {
566: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
567: }
568:
569: if ($propertiesCount === 1) {
570: return $propertyPrototypes[0];
571: }
572:
573: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes);
574: }
575:
576: public function hasStaticProperty(string $propertyName): TrinaryLogic
577: {
578: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName));
579: }
580:
581: public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
582: {
583: return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty();
584: }
585:
586: public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
587: {
588: $propertyPrototypes = [];
589: foreach ($this->types as $type) {
590: if (!$type->hasStaticProperty($propertyName)->yes()) {
591: continue;
592: }
593:
594: $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
595: }
596:
597: $propertiesCount = count($propertyPrototypes);
598: if ($propertiesCount === 0) {
599: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
600: }
601:
602: if ($propertiesCount === 1) {
603: return $propertyPrototypes[0];
604: }
605:
606: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes);
607: }
608:
609: public function canCallMethods(): TrinaryLogic
610: {
611: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
612: }
613:
614: public function hasMethod(string $methodName): TrinaryLogic
615: {
616: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
617: }
618:
619: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
620: {
621: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
622: }
623:
624: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
625: {
626: $methodPrototypes = [];
627: foreach ($this->types as $type) {
628: if (!$type->hasMethod($methodName)->yes()) {
629: continue;
630: }
631:
632: $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this);
633: }
634:
635: $methodsCount = count($methodPrototypes);
636: if ($methodsCount === 0) {
637: throw new MissingMethodFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $methodName);
638: }
639:
640: if ($methodsCount === 1) {
641: return $methodPrototypes[0];
642: }
643:
644: return new IntersectionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
645: }
646:
647: public function canAccessConstants(): TrinaryLogic
648: {
649: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
650: }
651:
652: public function hasConstant(string $constantName): TrinaryLogic
653: {
654: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName));
655: }
656:
657: public function getConstant(string $constantName): ClassConstantReflection
658: {
659: foreach ($this->types as $type) {
660: if ($type->hasConstant($constantName)->yes()) {
661: return $type->getConstant($constantName);
662: }
663: }
664:
665: throw new MissingConstantFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $constantName);
666: }
667:
668: public function isIterable(): TrinaryLogic
669: {
670: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
671: }
672:
673: public function isIterableAtLeastOnce(): TrinaryLogic
674: {
675: return $this->intersectResults(
676: static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce(),
677: static fn (Type $type): bool => !$type->isIterable()->no(),
678: );
679: }
680:
681: public function getArraySize(): Type
682: {
683: return $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize());
684: }
685:
686: public function getIterableKeyType(): Type
687: {
688: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
689: }
690:
691: public function getFirstIterableKeyType(): Type
692: {
693: return $this->intersectTypes(static fn (Type $type): Type => $type->getFirstIterableKeyType());
694: }
695:
696: public function getLastIterableKeyType(): Type
697: {
698: return $this->intersectTypes(static fn (Type $type): Type => $type->getLastIterableKeyType());
699: }
700:
701: public function getIterableValueType(): Type
702: {
703: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
704: }
705:
706: public function getFirstIterableValueType(): Type
707: {
708: return $this->intersectTypes(static fn (Type $type): Type => $type->getFirstIterableValueType());
709: }
710:
711: public function getLastIterableValueType(): Type
712: {
713: return $this->intersectTypes(static fn (Type $type): Type => $type->getLastIterableValueType());
714: }
715:
716: public function isArray(): TrinaryLogic
717: {
718: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray());
719: }
720:
721: public function isConstantArray(): TrinaryLogic
722: {
723: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray());
724: }
725:
726: public function isOversizedArray(): TrinaryLogic
727: {
728: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
729: }
730:
731: public function isList(): TrinaryLogic
732: {
733: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isList());
734: }
735:
736: public function isString(): TrinaryLogic
737: {
738: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString());
739: }
740:
741: public function isNumericString(): TrinaryLogic
742: {
743: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
744: }
745:
746: public function isNonEmptyString(): TrinaryLogic
747: {
748: if ($this->isCallable()->yes() && $this->isString()->yes()) {
749: return TrinaryLogic::createYes();
750: }
751: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
752: }
753:
754: public function isNonFalsyString(): TrinaryLogic
755: {
756: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
757: }
758:
759: public function isLiteralString(): TrinaryLogic
760: {
761: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
762: }
763:
764: public function isLowercaseString(): TrinaryLogic
765: {
766: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString());
767: }
768:
769: public function isUppercaseString(): TrinaryLogic
770: {
771: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString());
772: }
773:
774: public function isClassString(): TrinaryLogic
775: {
776: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassString());
777: }
778:
779: public function getClassStringObjectType(): Type
780: {
781: return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringObjectType());
782: }
783:
784: public function getObjectTypeOrClassStringObjectType(): Type
785: {
786: return $this->intersectTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType());
787: }
788:
789: public function isVoid(): TrinaryLogic
790: {
791: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isVoid());
792: }
793:
794: public function isScalar(): TrinaryLogic
795: {
796: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isScalar());
797: }
798:
799: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
800: {
801: return $this->intersectResults(
802: static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic(),
803: )->toBooleanType();
804: }
805:
806: public function isOffsetAccessible(): TrinaryLogic
807: {
808: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
809: }
810:
811: public function isOffsetAccessLegal(): TrinaryLogic
812: {
813: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
814: }
815:
816: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
817: {
818: if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) {
819: $arrayKeyOffsetType = $offsetType->toArrayKey();
820: if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
821: return TrinaryLogic::createYes();
822: }
823:
824: foreach ($this->types as $type) {
825: if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
826: continue;
827: }
828:
829: foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
830: if (!is_int($constantScalarValue)) {
831: continue;
832: }
833: if (IntegerRangeType::fromInterval(0, $constantScalarValue)->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
834: return TrinaryLogic::createYes();
835: }
836: }
837: }
838: }
839:
840: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
841: }
842:
843: public function getOffsetValueType(Type $offsetType): Type
844: {
845: $result = $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType));
846: if ($this->isOversizedArray()->yes()) {
847: return TypeUtils::toBenevolentUnion($result);
848: }
849:
850: return $result;
851: }
852:
853: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
854: {
855: if ($this->isOversizedArray()->yes()) {
856: return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type {
857: // avoid new HasOffsetValueType being intersected with oversized array
858: if (!$type instanceof ArrayType) {
859: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
860: }
861:
862: if (!$offsetType instanceof ConstantStringType && !$offsetType instanceof ConstantIntegerType) {
863: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
864: }
865:
866: if (!$offsetType->isSuperTypeOf($type->getKeyType())->yes()) {
867: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
868: }
869:
870: return TypeCombinator::intersect(
871: new ArrayType(
872: TypeCombinator::union($type->getKeyType(), $offsetType),
873: TypeCombinator::union($type->getItemType(), $valueType),
874: ),
875: new NonEmptyArrayType(),
876: );
877: });
878: }
879:
880: $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
881:
882: if (
883: $offsetType !== null
884: && $this->isList()->yes()
885: && !$result->isList()->yes()
886: ) {
887: if ($this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) {
888: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
889: } else {
890: foreach ($this->types as $type) {
891: if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
892: continue;
893: }
894:
895: foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
896: if (!is_int($constantScalarValue)) {
897: continue;
898: }
899: if (IntegerRangeType::fromInterval(0, $constantScalarValue + 1)->isSuperTypeOf($offsetType)->yes()) {
900: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
901: break 2;
902: }
903: }
904: }
905: }
906: }
907:
908: if ($this->isList()->yes() && $this->getIterableValueType()->isArray()->yes()) {
909: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
910: }
911:
912: return $result;
913: }
914:
915: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
916: {
917: return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
918: }
919:
920: public function unsetOffset(Type $offsetType): Type
921: {
922: return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
923: }
924:
925: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
926: {
927: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict));
928: }
929:
930: public function getKeysArray(): Type
931: {
932: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray());
933: }
934:
935: public function getValuesArray(): Type
936: {
937: return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray());
938: }
939:
940: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
941: {
942: return $this->intersectTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
943: }
944:
945: public function fillKeysArray(Type $valueType): Type
946: {
947: return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
948: }
949:
950: public function flipArray(): Type
951: {
952: return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray());
953: }
954:
955: public function intersectKeyArray(Type $otherArraysType): Type
956: {
957: return $this->intersectTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
958: }
959:
960: public function popArray(): Type
961: {
962: return $this->intersectTypes(static fn (Type $type): Type => $type->popArray());
963: }
964:
965: public function reverseArray(TrinaryLogic $preserveKeys): Type
966: {
967: return $this->intersectTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys));
968: }
969:
970: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
971: {
972: return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict));
973: }
974:
975: public function shiftArray(): Type
976: {
977: return $this->intersectTypes(static fn (Type $type): Type => $type->shiftArray());
978: }
979:
980: public function shuffleArray(): Type
981: {
982: return $this->intersectTypes(static fn (Type $type): Type => $type->shuffleArray());
983: }
984:
985: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
986: {
987: $result = $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
988:
989: if (
990: $this->isList()->yes()
991: && $this->isIterableAtLeastOnce()->yes()
992: && (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()
993: && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()
994: ) {
995: $result = TypeCombinator::intersect($result, new NonEmptyArrayType());
996: }
997:
998: return $result;
999: }
1000:
1001: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1002: {
1003: return $this->intersectTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
1004: }
1005:
1006: public function getEnumCases(): array
1007: {
1008: $compare = [];
1009: foreach ($this->types as $type) {
1010: $oneType = [];
1011: foreach ($type->getEnumCases() as $enumCase) {
1012: $oneType[$enumCase->getClassName() . '::' . $enumCase->getEnumCaseName()] = $enumCase;
1013: }
1014: $compare[] = $oneType;
1015: }
1016:
1017: return array_values(array_intersect_key(...$compare));
1018: }
1019:
1020: public function isCallable(): TrinaryLogic
1021: {
1022: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
1023: }
1024:
1025: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
1026: {
1027: if ($this->isCallable()->no()) {
1028: throw new ShouldNotHappenException();
1029: }
1030:
1031: return [new TrivialParametersAcceptor()];
1032: }
1033:
1034: public function isCloneable(): TrinaryLogic
1035: {
1036: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
1037: }
1038:
1039: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1040: {
1041: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion));
1042: }
1043:
1044: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1045: {
1046: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion));
1047: }
1048:
1049: public function isNull(): TrinaryLogic
1050: {
1051: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNull());
1052: }
1053:
1054: public function isConstantValue(): TrinaryLogic
1055: {
1056: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
1057: }
1058:
1059: public function isConstantScalarValue(): TrinaryLogic
1060: {
1061: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
1062: }
1063:
1064: public function getConstantScalarTypes(): array
1065: {
1066: $scalarTypes = [];
1067: foreach ($this->types as $type) {
1068: foreach ($type->getConstantScalarTypes() as $scalarType) {
1069: $scalarTypes[] = $scalarType;
1070: }
1071: }
1072:
1073: return $scalarTypes;
1074: }
1075:
1076: public function getConstantScalarValues(): array
1077: {
1078: $values = [];
1079: foreach ($this->types as $type) {
1080: foreach ($type->getConstantScalarValues() as $value) {
1081: $values[] = $value;
1082: }
1083: }
1084:
1085: return $values;
1086: }
1087:
1088: public function isTrue(): TrinaryLogic
1089: {
1090: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
1091: }
1092:
1093: public function isFalse(): TrinaryLogic
1094: {
1095: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
1096: }
1097:
1098: public function isBoolean(): TrinaryLogic
1099: {
1100: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
1101: }
1102:
1103: public function isFloat(): TrinaryLogic
1104: {
1105: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
1106: }
1107:
1108: public function isInteger(): TrinaryLogic
1109: {
1110: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
1111: }
1112:
1113: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1114: {
1115: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion));
1116: }
1117:
1118: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1119: {
1120: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion));
1121: }
1122:
1123: public function getSmallerType(PhpVersion $phpVersion): Type
1124: {
1125: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion));
1126: }
1127:
1128: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
1129: {
1130: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion));
1131: }
1132:
1133: public function getGreaterType(PhpVersion $phpVersion): Type
1134: {
1135: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion));
1136: }
1137:
1138: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
1139: {
1140: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion));
1141: }
1142:
1143: public function toBoolean(): BooleanType
1144: {
1145: $type = $this->intersectTypes(static fn (Type $type): BooleanType => $type->toBoolean());
1146:
1147: if (!$type instanceof BooleanType) {
1148: return new BooleanType();
1149: }
1150:
1151: return $type;
1152: }
1153:
1154: public function toNumber(): Type
1155: {
1156: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toNumber());
1157:
1158: return $type;
1159: }
1160:
1161: public function toAbsoluteNumber(): Type
1162: {
1163: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());
1164:
1165: return $type;
1166: }
1167:
1168: public function toString(): Type
1169: {
1170: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString());
1171:
1172: return $type;
1173: }
1174:
1175: public function toInteger(): Type
1176: {
1177: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toInteger());
1178:
1179: return $type;
1180: }
1181:
1182: public function toFloat(): Type
1183: {
1184: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toFloat());
1185:
1186: return $type;
1187: }
1188:
1189: public function toArray(): Type
1190: {
1191: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toArray());
1192:
1193: return $type;
1194: }
1195:
1196: public function toArrayKey(): Type
1197: {
1198: if ($this->isNumericString()->yes()) {
1199: return TypeCombinator::union(
1200: new IntegerType(),
1201: $this,
1202: );
1203: }
1204:
1205: if ($this->isString()->yes()) {
1206: return $this;
1207: }
1208:
1209: return $this->intersectTypes(static fn (Type $type): Type => $type->toArrayKey());
1210: }
1211:
1212: public function toCoercedArgumentType(bool $strictTypes): Type
1213: {
1214: return $this->intersectTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes));
1215: }
1216:
1217: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1218: {
1219: $types = TemplateTypeMap::createEmpty();
1220:
1221: foreach ($this->types as $type) {
1222: $types = $types->intersect($type->inferTemplateTypes($receivedType));
1223: }
1224:
1225: return $types;
1226: }
1227:
1228: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1229: {
1230: $references = [];
1231:
1232: foreach ($this->types as $type) {
1233: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
1234: $references[] = $reference;
1235: }
1236: }
1237:
1238: return $references;
1239: }
1240:
1241: public function traverse(callable $cb): Type
1242: {
1243: $types = [];
1244: $changed = false;
1245:
1246: foreach ($this->types as $type) {
1247: $newType = $cb($type);
1248: if ($type !== $newType) {
1249: $changed = true;
1250: }
1251: $types[] = $newType;
1252: }
1253:
1254: if ($changed) {
1255: return TypeCombinator::intersect(...$types);
1256: }
1257:
1258: return $this;
1259: }
1260:
1261: public function traverseSimultaneously(Type $right, callable $cb): Type
1262: {
1263: $types = [];
1264: $changed = false;
1265:
1266: if (!$right instanceof self) {
1267: return $this;
1268: }
1269:
1270: if (count($this->getTypes()) !== count($right->getTypes())) {
1271: return $this;
1272: }
1273:
1274: foreach ($this->getSortedTypes() as $i => $leftType) {
1275: $rightType = $right->getSortedTypes()[$i];
1276: $newType = $cb($leftType, $rightType);
1277: if ($leftType !== $newType) {
1278: $changed = true;
1279: }
1280: $types[] = $newType;
1281: }
1282:
1283: if ($changed) {
1284: return TypeCombinator::intersect(...$types);
1285: }
1286:
1287: return $this;
1288: }
1289:
1290: public function tryRemove(Type $typeToRemove): ?Type
1291: {
1292: return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1293: }
1294:
1295: public function exponentiate(Type $exponent): Type
1296: {
1297: return $this->intersectTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1298: }
1299:
1300: public function getFiniteTypes(): array
1301: {
1302: $compare = [];
1303: foreach ($this->types as $type) {
1304: $oneType = [];
1305: foreach ($type->getFiniteTypes() as $finiteType) {
1306: $oneType[md5($finiteType->describe(VerbosityLevel::typeOnly()))] = $finiteType;
1307: }
1308: $compare[] = $oneType;
1309: }
1310:
1311: $result = array_values(array_intersect_key(...$compare));
1312:
1313: if (count($result) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1314: return [];
1315: }
1316:
1317: return $result;
1318: }
1319:
1320: /**
1321: * @param callable(Type $type): TrinaryLogic $getResult
1322: * @param (callable(Type $type): bool)|null $filter
1323: */
1324: private function intersectResults(
1325: callable $getResult,
1326: ?callable $filter = null,
1327: ): TrinaryLogic
1328: {
1329: $types = $this->types;
1330: if ($filter !== null) {
1331: $types = array_filter($types, $filter);
1332: }
1333: if (count($types) === 0) {
1334: return TrinaryLogic::createNo();
1335: }
1336:
1337: return TrinaryLogic::lazyMaxMin($types, $getResult);
1338: }
1339:
1340: /**
1341: * @param callable(Type $type): Type $getType
1342: */
1343: private function intersectTypes(callable $getType): Type
1344: {
1345: $operands = array_map($getType, $this->types);
1346: return TypeCombinator::intersect(...$operands);
1347: }
1348:
1349: public function toPhpDocNode(): TypeNode
1350: {
1351: $baseTypes = [];
1352: $typesToDescribe = [];
1353: $skipTypeNames = [];
1354:
1355: $nonEmptyStr = false;
1356: $nonFalsyStr = false;
1357: $isList = $this->isList()->yes();
1358: $isArray = $this->isArray()->yes();
1359: $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
1360: $describedTypes = [];
1361:
1362: foreach ($this->getSortedTypes() as $i => $type) {
1363: if ($type instanceof AccessoryNonEmptyStringType
1364: || $type instanceof AccessoryLiteralStringType
1365: || $type instanceof AccessoryNumericStringType
1366: || $type instanceof AccessoryNonFalsyStringType
1367: || $type instanceof AccessoryLowercaseStringType
1368: || $type instanceof AccessoryUppercaseStringType
1369: ) {
1370: if ($type instanceof AccessoryNonFalsyStringType) {
1371: $nonFalsyStr = true;
1372: }
1373: if ($type instanceof AccessoryNonEmptyStringType) {
1374: $nonEmptyStr = true;
1375: }
1376: if ($nonEmptyStr && $nonFalsyStr) {
1377: // prevent redundant 'non-empty-string&non-falsy-string'
1378: foreach ($typesToDescribe as $key => $typeToDescribe) {
1379: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
1380: continue;
1381: }
1382:
1383: unset($typesToDescribe[$key]);
1384: }
1385: }
1386:
1387: $typesToDescribe[$i] = $type;
1388: $skipTypeNames[] = 'string';
1389: continue;
1390: }
1391:
1392: if ($isList || $isArray) {
1393: if ($type instanceof ArrayType) {
1394: $keyType = $type->getKeyType();
1395: $valueType = $type->getItemType();
1396: if ($isList) {
1397: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1398: $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-list' : 'list');
1399: if (!$isMixedValueType) {
1400: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1401: $valueType->toPhpDocNode(),
1402: ]);
1403: } else {
1404: $describedTypes[$i] = $identifierTypeNode;
1405: }
1406: } else {
1407: $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
1408: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1409: $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-array' : 'array');
1410: if (!$isMixedKeyType) {
1411: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1412: $keyType->toPhpDocNode(),
1413: $valueType->toPhpDocNode(),
1414: ]);
1415: } elseif (!$isMixedValueType) {
1416: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1417: $valueType->toPhpDocNode(),
1418: ]);
1419: } else {
1420: $describedTypes[$i] = $identifierTypeNode;
1421: }
1422: }
1423: continue;
1424: } elseif ($type instanceof ConstantArrayType) {
1425: $constantArrayTypeNode = $type->toPhpDocNode();
1426: if ($constantArrayTypeNode instanceof ArrayShapeNode) {
1427: $newKind = $constantArrayTypeNode->kind;
1428: if ($isList) {
1429: if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1430: $newKind = ArrayShapeNode::KIND_NON_EMPTY_LIST;
1431: } else {
1432: $newKind = ArrayShapeNode::KIND_LIST;
1433: }
1434: } elseif ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1435: $newKind = ArrayShapeNode::KIND_NON_EMPTY_ARRAY;
1436: }
1437:
1438: if ($newKind !== $constantArrayTypeNode->kind) {
1439: if ($constantArrayTypeNode->sealed) {
1440: $constantArrayTypeNode = ArrayShapeNode::createSealed($constantArrayTypeNode->items, $newKind);
1441: } else {
1442: $constantArrayTypeNode = ArrayShapeNode::createUnsealed($constantArrayTypeNode->items, $constantArrayTypeNode->unsealedType, $newKind);
1443: }
1444: }
1445:
1446: $describedTypes[$i] = $constantArrayTypeNode;
1447: continue;
1448: }
1449: }
1450: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
1451: continue;
1452: }
1453: }
1454:
1455: if (!$type instanceof AccessoryType) {
1456: $baseTypes[$i] = $type;
1457: continue;
1458: }
1459:
1460: $accessoryPhpDocNode = $type->toPhpDocNode();
1461: if ($accessoryPhpDocNode instanceof IdentifierTypeNode && $accessoryPhpDocNode->name === '') {
1462: continue;
1463: }
1464:
1465: $typesToDescribe[$i] = $type;
1466: }
1467:
1468: foreach ($baseTypes as $i => $type) {
1469: $typeNode = $type->toPhpDocNode();
1470: if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') {
1471: $nonEmpty = false;
1472: $typeName = 'array';
1473: foreach ($typesToDescribe as $j => $typeToDescribe) {
1474: if ($typeToDescribe instanceof AccessoryArrayListType) {
1475: $typeName = 'list';
1476: if (count($typeNode->genericTypes) > 1) {
1477: array_shift($typeNode->genericTypes);
1478: }
1479: } elseif ($typeToDescribe instanceof NonEmptyArrayType) {
1480: $nonEmpty = true;
1481: } else {
1482: continue;
1483: }
1484:
1485: unset($typesToDescribe[$j]);
1486: }
1487:
1488: if ($nonEmpty) {
1489: $typeName = 'non-empty-' . $typeName;
1490: }
1491:
1492: $describedTypes[$i] = new GenericTypeNode(
1493: new IdentifierTypeNode($typeName),
1494: $typeNode->genericTypes,
1495: );
1496: continue;
1497: }
1498:
1499: if ($typeNode instanceof IdentifierTypeNode && in_array($typeNode->name, $skipTypeNames, true)) {
1500: continue;
1501: }
1502:
1503: $describedTypes[$i] = $typeNode;
1504: }
1505:
1506: foreach ($typesToDescribe as $i => $typeToDescribe) {
1507: $describedTypes[$i] = $typeToDescribe->toPhpDocNode();
1508: }
1509:
1510: ksort($describedTypes);
1511:
1512: $describedTypes = array_values($describedTypes);
1513:
1514: if (count($describedTypes) === 1) {
1515: return $describedTypes[0];
1516: }
1517:
1518: return new IntersectionTypeNode($describedTypes);
1519: }
1520:
1521: }
1522: