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\Enum\EnumCaseObjectType;
41: use PHPStan\Type\Generic\TemplateType;
42: use PHPStan\Type\Generic\TemplateTypeMap;
43: use PHPStan\Type\Generic\TemplateTypeVariance;
44: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
45: use PHPStan\Type\Traits\NonRemoveableTypeTrait;
46: use function array_filter;
47: use function array_intersect_key;
48: use function array_map;
49: use function array_shift;
50: use function array_unique;
51: use function array_values;
52: use function count;
53: use function implode;
54: use function in_array;
55: use function is_int;
56: use function ksort;
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 list<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 list<Type>
89: */
90: public function getTypes(): array
91: {
92: return $this->types;
93: }
94:
95: /**
96: * @return list<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: fn (): string => $this->describeType($level),
308: fn (): string => $this->describeItself($level, true),
309: fn (): string => $this->describeItself($level, false),
310: );
311: }
312:
313: private function describeType(VerbosityLevel $level): string
314: {
315: $typeNames = [];
316: $isList = $this->isList()->yes();
317: $valueType = null;
318: foreach ($this->getSortedTypes() as $type) {
319: if ($isList) {
320: if ($type instanceof ArrayType || $type instanceof ConstantArrayType) {
321: $valueType = $type->getIterableValueType();
322: continue;
323: }
324: if ($type instanceof NonEmptyArrayType) {
325: continue;
326: }
327: }
328: if ($type instanceof AccessoryType) {
329: continue;
330: }
331: $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level);
332: }
333:
334: if ($isList) {
335: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
336: $innerType = '';
337: if ($valueType !== null && !$isMixedValueType) {
338: $innerType = sprintf('<%s>', $valueType->describe($level));
339: }
340:
341: $typeNames[] = 'list' . $innerType;
342: }
343:
344: usort($typeNames, static function ($a, $b) {
345: $cmp = strcasecmp($a, $b);
346: if ($cmp !== 0) {
347: return $cmp;
348: }
349:
350: return $a <=> $b;
351: });
352:
353: return implode('&', $typeNames);
354: }
355:
356: private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string
357: {
358: $baseTypes = [];
359: $typesToDescribe = [];
360: $skipTypeNames = [];
361:
362: $nonEmptyStr = false;
363: $nonFalsyStr = false;
364: $isList = $this->isList()->yes();
365: $isArray = $this->isArray()->yes();
366: $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
367: $describedTypes = [];
368: foreach ($this->getSortedTypes() as $i => $type) {
369: if ($type instanceof AccessoryNonEmptyStringType
370: || $type instanceof AccessoryLiteralStringType
371: || $type instanceof AccessoryNumericStringType
372: || $type instanceof AccessoryNonFalsyStringType
373: || $type instanceof AccessoryLowercaseStringType
374: || $type instanceof AccessoryUppercaseStringType
375: ) {
376: if (
377: ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType)
378: && !$level->isPrecise()
379: && !$level->isCache()
380: ) {
381: continue;
382: }
383: if ($type instanceof AccessoryNonFalsyStringType) {
384: $nonFalsyStr = true;
385: }
386: if ($type instanceof AccessoryNonEmptyStringType) {
387: $nonEmptyStr = true;
388: }
389: if ($nonEmptyStr && $nonFalsyStr) {
390: // prevent redundant 'non-empty-string&non-falsy-string'
391: foreach ($typesToDescribe as $key => $typeToDescribe) {
392: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
393: continue;
394: }
395:
396: unset($typesToDescribe[$key]);
397: }
398: }
399:
400: $typesToDescribe[$i] = $type;
401: $skipTypeNames[] = 'string';
402: continue;
403: }
404: if ($isList || $isArray) {
405: if ($type instanceof ArrayType) {
406: $keyType = $type->getKeyType();
407: $valueType = $type->getItemType();
408: if ($isList) {
409: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
410: $valueTypeDescription = '';
411: if (!$isMixedValueType) {
412: $valueTypeDescription = sprintf('<%s>', $valueType->describe($level));
413: }
414:
415: $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-list' : 'list') . $valueTypeDescription;
416: } else {
417: $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
418: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
419: $typeDescription = '';
420: if (!$isMixedKeyType) {
421: $typeDescription = sprintf('<%s, %s>', $keyType->describe($level), $valueType->describe($level));
422: } elseif (!$isMixedValueType) {
423: $typeDescription = sprintf('<%s>', $valueType->describe($level));
424: }
425:
426: $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-array' : 'array') . $typeDescription;
427: }
428: continue;
429: } elseif ($type instanceof ConstantArrayType) {
430: $description = $type->describe($level);
431: $descriptionWithoutKind = substr($description, strlen('array'));
432: $begin = $isList ? 'list' : 'array';
433: if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
434: $begin = 'non-empty-' . $begin;
435: }
436:
437: $describedTypes[$i] = $begin . $descriptionWithoutKind;
438: continue;
439: }
440: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
441: continue;
442: }
443: }
444:
445: if ($type instanceof CallableType && $type->isCommonCallable()) {
446: $typesToDescribe[$i] = $type;
447: $skipTypeNames[] = 'object';
448: $skipTypeNames[] = 'string';
449: continue;
450: }
451:
452: if (!$type instanceof AccessoryType) {
453: $baseTypes[$i] = $type;
454: continue;
455: }
456:
457: if ($skipAccessoryTypes) {
458: continue;
459: }
460:
461: $typesToDescribe[$i] = $type;
462: }
463:
464: foreach ($baseTypes as $i => $type) {
465: $typeDescription = $type->describe($level);
466:
467: if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) {
468: foreach ($typesToDescribe as $j => $typeToDescribe) {
469: if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) {
470: $describedTypes[$i] = 'callable-' . $typeDescription;
471: unset($typesToDescribe[$j]);
472: continue 2;
473: }
474: }
475: }
476:
477: if (in_array($typeDescription, $skipTypeNames, true)) {
478: continue;
479: }
480:
481: $describedTypes[$i] = $type->describe($level);
482: }
483:
484: foreach ($typesToDescribe as $i => $typeToDescribe) {
485: $describedTypes[$i] = $typeToDescribe->describe($level);
486: }
487:
488: ksort($describedTypes);
489:
490: return implode('&', $describedTypes);
491: }
492:
493: public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type
494: {
495: return $this->intersectTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName));
496: }
497:
498: public function isObject(): TrinaryLogic
499: {
500: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isObject());
501: }
502:
503: public function isEnum(): TrinaryLogic
504: {
505: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isEnum());
506: }
507:
508: public function canAccessProperties(): TrinaryLogic
509: {
510: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
511: }
512:
513: public function hasProperty(string $propertyName): TrinaryLogic
514: {
515: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
516: }
517:
518: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
519: {
520: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
521: }
522:
523: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
524: {
525: $propertyPrototypes = [];
526: foreach ($this->types as $type) {
527: if (!$type->hasProperty($propertyName)->yes()) {
528: continue;
529: }
530:
531: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
532: }
533:
534: $propertiesCount = count($propertyPrototypes);
535: if ($propertiesCount === 0) {
536: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
537: }
538:
539: if ($propertiesCount === 1) {
540: return $propertyPrototypes[0];
541: }
542:
543: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes);
544: }
545:
546: public function hasInstanceProperty(string $propertyName): TrinaryLogic
547: {
548: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName));
549: }
550:
551: public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
552: {
553: return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty();
554: }
555:
556: public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
557: {
558: $propertyPrototypes = [];
559: foreach ($this->types as $type) {
560: if (!$type->hasInstanceProperty($propertyName)->yes()) {
561: continue;
562: }
563:
564: $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this);
565: }
566:
567: $propertiesCount = count($propertyPrototypes);
568: if ($propertiesCount === 0) {
569: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
570: }
571:
572: if ($propertiesCount === 1) {
573: return $propertyPrototypes[0];
574: }
575:
576: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes);
577: }
578:
579: public function hasStaticProperty(string $propertyName): TrinaryLogic
580: {
581: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName));
582: }
583:
584: public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
585: {
586: return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty();
587: }
588:
589: public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
590: {
591: $propertyPrototypes = [];
592: foreach ($this->types as $type) {
593: if (!$type->hasStaticProperty($propertyName)->yes()) {
594: continue;
595: }
596:
597: $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
598: }
599:
600: $propertiesCount = count($propertyPrototypes);
601: if ($propertiesCount === 0) {
602: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
603: }
604:
605: if ($propertiesCount === 1) {
606: return $propertyPrototypes[0];
607: }
608:
609: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes);
610: }
611:
612: public function canCallMethods(): TrinaryLogic
613: {
614: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
615: }
616:
617: public function hasMethod(string $methodName): TrinaryLogic
618: {
619: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
620: }
621:
622: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
623: {
624: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
625: }
626:
627: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
628: {
629: $methodPrototypes = [];
630: foreach ($this->types as $type) {
631: if (!$type->hasMethod($methodName)->yes()) {
632: continue;
633: }
634:
635: $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this);
636: }
637:
638: $methodsCount = count($methodPrototypes);
639: if ($methodsCount === 0) {
640: throw new MissingMethodFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $methodName);
641: }
642:
643: if ($methodsCount === 1) {
644: return $methodPrototypes[0];
645: }
646:
647: return new IntersectionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
648: }
649:
650: public function canAccessConstants(): TrinaryLogic
651: {
652: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
653: }
654:
655: public function hasConstant(string $constantName): TrinaryLogic
656: {
657: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName));
658: }
659:
660: public function getConstant(string $constantName): ClassConstantReflection
661: {
662: foreach ($this->types as $type) {
663: if ($type->hasConstant($constantName)->yes()) {
664: return $type->getConstant($constantName);
665: }
666: }
667:
668: throw new MissingConstantFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $constantName);
669: }
670:
671: public function isIterable(): TrinaryLogic
672: {
673: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
674: }
675:
676: public function isIterableAtLeastOnce(): TrinaryLogic
677: {
678: return $this->intersectResults(
679: static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce(),
680: static fn (Type $type): bool => !$type->isIterable()->no(),
681: );
682: }
683:
684: public function getArraySize(): Type
685: {
686: $arraySize = $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize());
687:
688: $knownOffsets = [];
689: foreach ($this->types as $type) {
690: if (!($type instanceof HasOffsetValueType) && !($type instanceof HasOffsetType)) {
691: continue;
692: }
693:
694: $knownOffsets[$type->getOffsetType()->getValue()] = true;
695: }
696:
697: if ($knownOffsets !== []) {
698: return TypeCombinator::intersect($arraySize, IntegerRangeType::fromInterval(count($knownOffsets), null));
699: }
700:
701: return $arraySize;
702: }
703:
704: public function getIterableKeyType(): Type
705: {
706: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
707: }
708:
709: public function getFirstIterableKeyType(): Type
710: {
711: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
712: }
713:
714: public function getLastIterableKeyType(): Type
715: {
716: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
717: }
718:
719: public function getIterableValueType(): Type
720: {
721: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
722: }
723:
724: public function getFirstIterableValueType(): Type
725: {
726: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
727: }
728:
729: public function getLastIterableValueType(): Type
730: {
731: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
732: }
733:
734: public function isArray(): TrinaryLogic
735: {
736: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray());
737: }
738:
739: public function isConstantArray(): TrinaryLogic
740: {
741: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray());
742: }
743:
744: public function isOversizedArray(): TrinaryLogic
745: {
746: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
747: }
748:
749: public function isList(): TrinaryLogic
750: {
751: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isList());
752: }
753:
754: public function isString(): TrinaryLogic
755: {
756: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString());
757: }
758:
759: public function isNumericString(): TrinaryLogic
760: {
761: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
762: }
763:
764: public function isNonEmptyString(): TrinaryLogic
765: {
766: if ($this->isCallable()->yes() && $this->isString()->yes()) {
767: return TrinaryLogic::createYes();
768: }
769: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
770: }
771:
772: public function isNonFalsyString(): TrinaryLogic
773: {
774: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
775: }
776:
777: public function isLiteralString(): TrinaryLogic
778: {
779: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
780: }
781:
782: public function isLowercaseString(): TrinaryLogic
783: {
784: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString());
785: }
786:
787: public function isUppercaseString(): TrinaryLogic
788: {
789: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString());
790: }
791:
792: public function isClassString(): TrinaryLogic
793: {
794: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassString());
795: }
796:
797: public function getClassStringObjectType(): Type
798: {
799: return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringObjectType());
800: }
801:
802: public function getObjectTypeOrClassStringObjectType(): Type
803: {
804: return $this->intersectTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType());
805: }
806:
807: public function isVoid(): TrinaryLogic
808: {
809: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isVoid());
810: }
811:
812: public function isScalar(): TrinaryLogic
813: {
814: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isScalar());
815: }
816:
817: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
818: {
819: return $this->intersectResults(
820: static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic(),
821: )->toBooleanType();
822: }
823:
824: public function isOffsetAccessible(): TrinaryLogic
825: {
826: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
827: }
828:
829: public function isOffsetAccessLegal(): TrinaryLogic
830: {
831: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
832: }
833:
834: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
835: {
836: if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) {
837: $arrayKeyOffsetType = $offsetType->toArrayKey();
838: if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
839: return TrinaryLogic::createYes();
840: }
841:
842: foreach ($this->types as $type) {
843: if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
844: continue;
845: }
846:
847: foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
848: if (!is_int($constantScalarValue)) {
849: continue;
850: }
851: if (IntegerRangeType::fromInterval(0, $constantScalarValue)->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
852: return TrinaryLogic::createYes();
853: }
854: }
855: }
856: }
857:
858: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
859: }
860:
861: public function getOffsetValueType(Type $offsetType): Type
862: {
863: $result = $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType));
864: if ($this->isOversizedArray()->yes()) {
865: return TypeUtils::toBenevolentUnion($result);
866: }
867:
868: return $result;
869: }
870:
871: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
872: {
873: if ($this->isOversizedArray()->yes()) {
874: return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type {
875: // avoid new HasOffsetValueType being intersected with oversized array
876: if (!$type instanceof ArrayType) {
877: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
878: }
879:
880: if (!$offsetType instanceof ConstantStringType && !$offsetType instanceof ConstantIntegerType) {
881: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
882: }
883:
884: if (!$offsetType->isSuperTypeOf($type->getKeyType())->yes()) {
885: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
886: }
887:
888: return new IntersectionType([
889: new ArrayType(
890: TypeCombinator::union($type->getKeyType(), $offsetType),
891: TypeCombinator::union($type->getItemType(), $valueType),
892: ),
893: new NonEmptyArrayType(),
894: ]);
895: });
896: }
897:
898: $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
899:
900: if (
901: $offsetType !== null
902: && $this->isList()->yes()
903: && !$result->isList()->yes()
904: ) {
905: if ($this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) {
906: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
907: } else {
908: foreach ($this->types as $type) {
909: if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
910: continue;
911: }
912:
913: foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
914: if (!is_int($constantScalarValue)) {
915: continue;
916: }
917: if (IntegerRangeType::fromInterval(0, $constantScalarValue + 1)->isSuperTypeOf($offsetType)->yes()) {
918: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
919: break 2;
920: }
921: }
922: }
923: }
924: }
925:
926: if ($this->isList()->yes() && $this->getIterableValueType()->isArray()->yes()) {
927: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
928: }
929:
930: return $result;
931: }
932:
933: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
934: {
935: return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
936: }
937:
938: public function unsetOffset(Type $offsetType): Type
939: {
940: return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
941: }
942:
943: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
944: {
945: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict));
946: }
947:
948: public function getKeysArray(): Type
949: {
950: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray());
951: }
952:
953: public function getValuesArray(): Type
954: {
955: return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray());
956: }
957:
958: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
959: {
960: return $this->intersectTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
961: }
962:
963: public function fillKeysArray(Type $valueType): Type
964: {
965: return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
966: }
967:
968: public function flipArray(): Type
969: {
970: return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray());
971: }
972:
973: public function intersectKeyArray(Type $otherArraysType): Type
974: {
975: return $this->intersectTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
976: }
977:
978: public function popArray(): Type
979: {
980: return $this->intersectTypes(static fn (Type $type): Type => $type->popArray());
981: }
982:
983: public function reverseArray(TrinaryLogic $preserveKeys): Type
984: {
985: return $this->intersectTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys));
986: }
987:
988: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
989: {
990: return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict));
991: }
992:
993: public function shiftArray(): Type
994: {
995: return $this->intersectTypes(static fn (Type $type): Type => $type->shiftArray());
996: }
997:
998: public function shuffleArray(): Type
999: {
1000: return $this->intersectTypes(static fn (Type $type): Type => $type->shuffleArray());
1001: }
1002:
1003: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
1004: {
1005: $result = $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
1006:
1007: if (
1008: $this->isList()->yes()
1009: && $this->isIterableAtLeastOnce()->yes()
1010: && (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()
1011: && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()
1012: ) {
1013: $result = TypeCombinator::intersect($result, new NonEmptyArrayType());
1014: }
1015:
1016: return $result;
1017: }
1018:
1019: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1020: {
1021: return $this->intersectTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
1022: }
1023:
1024: public function getEnumCases(): array
1025: {
1026: $compare = [];
1027: foreach ($this->types as $type) {
1028: $oneType = [];
1029: foreach ($type->getEnumCases() as $enumCase) {
1030: $oneType[$enumCase->getClassName() . '::' . $enumCase->getEnumCaseName()] = $enumCase;
1031: }
1032: $compare[] = $oneType;
1033: }
1034:
1035: return array_values(array_intersect_key(...$compare));
1036: }
1037:
1038: public function getEnumCaseObject(): ?EnumCaseObjectType
1039: {
1040: $singleCase = null;
1041: foreach ($this->types as $type) {
1042: $caseObject = $type->getEnumCaseObject();
1043: if ($caseObject === null) {
1044: continue;
1045: }
1046:
1047: if ($singleCase !== null) {
1048: return null;
1049: }
1050:
1051: $singleCase = $caseObject;
1052: }
1053:
1054: return $singleCase;
1055: }
1056:
1057: public function isCallable(): TrinaryLogic
1058: {
1059: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
1060: }
1061:
1062: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
1063: {
1064: if ($this->isCallable()->no()) {
1065: throw new ShouldNotHappenException();
1066: }
1067:
1068: return [new TrivialParametersAcceptor()];
1069: }
1070:
1071: public function isCloneable(): TrinaryLogic
1072: {
1073: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
1074: }
1075:
1076: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1077: {
1078: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion));
1079: }
1080:
1081: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1082: {
1083: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion));
1084: }
1085:
1086: public function isNull(): TrinaryLogic
1087: {
1088: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNull());
1089: }
1090:
1091: public function isConstantValue(): TrinaryLogic
1092: {
1093: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
1094: }
1095:
1096: public function isConstantScalarValue(): TrinaryLogic
1097: {
1098: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
1099: }
1100:
1101: public function getConstantScalarTypes(): array
1102: {
1103: $scalarTypes = [];
1104: foreach ($this->types as $type) {
1105: foreach ($type->getConstantScalarTypes() as $scalarType) {
1106: $scalarTypes[] = $scalarType;
1107: }
1108: }
1109:
1110: return $scalarTypes;
1111: }
1112:
1113: public function getConstantScalarValues(): array
1114: {
1115: $values = [];
1116: foreach ($this->types as $type) {
1117: foreach ($type->getConstantScalarValues() as $value) {
1118: $values[] = $value;
1119: }
1120: }
1121:
1122: return $values;
1123: }
1124:
1125: public function isTrue(): TrinaryLogic
1126: {
1127: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
1128: }
1129:
1130: public function isFalse(): TrinaryLogic
1131: {
1132: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
1133: }
1134:
1135: public function isBoolean(): TrinaryLogic
1136: {
1137: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
1138: }
1139:
1140: public function isFloat(): TrinaryLogic
1141: {
1142: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
1143: }
1144:
1145: public function isInteger(): TrinaryLogic
1146: {
1147: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
1148: }
1149:
1150: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1151: {
1152: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion));
1153: }
1154:
1155: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1156: {
1157: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion));
1158: }
1159:
1160: public function getSmallerType(PhpVersion $phpVersion): Type
1161: {
1162: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion));
1163: }
1164:
1165: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
1166: {
1167: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion));
1168: }
1169:
1170: public function getGreaterType(PhpVersion $phpVersion): Type
1171: {
1172: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion));
1173: }
1174:
1175: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
1176: {
1177: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion));
1178: }
1179:
1180: public function toBoolean(): BooleanType
1181: {
1182: $type = $this->intersectTypes(static fn (Type $type): BooleanType => $type->toBoolean());
1183:
1184: if (!$type instanceof BooleanType) {
1185: return new BooleanType();
1186: }
1187:
1188: return $type;
1189: }
1190:
1191: public function toNumber(): Type
1192: {
1193: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toNumber());
1194:
1195: return $type;
1196: }
1197:
1198: public function toAbsoluteNumber(): Type
1199: {
1200: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());
1201:
1202: return $type;
1203: }
1204:
1205: public function toString(): Type
1206: {
1207: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString());
1208:
1209: return $type;
1210: }
1211:
1212: public function toInteger(): Type
1213: {
1214: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toInteger());
1215:
1216: return $type;
1217: }
1218:
1219: public function toFloat(): Type
1220: {
1221: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toFloat());
1222:
1223: return $type;
1224: }
1225:
1226: public function toArray(): Type
1227: {
1228: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toArray());
1229:
1230: return $type;
1231: }
1232:
1233: public function toArrayKey(): Type
1234: {
1235: if ($this->isNumericString()->yes()) {
1236: return TypeCombinator::union(
1237: new IntegerType(),
1238: $this,
1239: );
1240: }
1241:
1242: if ($this->isString()->yes()) {
1243: return $this;
1244: }
1245:
1246: return $this->intersectTypes(static fn (Type $type): Type => $type->toArrayKey());
1247: }
1248:
1249: public function toCoercedArgumentType(bool $strictTypes): Type
1250: {
1251: return $this->intersectTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes));
1252: }
1253:
1254: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1255: {
1256: $types = TemplateTypeMap::createEmpty();
1257:
1258: foreach ($this->types as $type) {
1259: $types = $types->intersect($type->inferTemplateTypes($receivedType));
1260: }
1261:
1262: return $types;
1263: }
1264:
1265: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1266: {
1267: $references = [];
1268:
1269: foreach ($this->types as $type) {
1270: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
1271: $references[] = $reference;
1272: }
1273: }
1274:
1275: return $references;
1276: }
1277:
1278: public function traverse(callable $cb): Type
1279: {
1280: $types = [];
1281: $changed = false;
1282:
1283: foreach ($this->types as $type) {
1284: $newType = $cb($type);
1285: if ($type !== $newType) {
1286: $changed = true;
1287: }
1288: $types[] = $newType;
1289: }
1290:
1291: if ($changed) {
1292: return TypeCombinator::intersect(...$types);
1293: }
1294:
1295: return $this;
1296: }
1297:
1298: public function traverseSimultaneously(Type $right, callable $cb): Type
1299: {
1300: if ($this->isArray()->yes() && $right->isArray()->yes()) {
1301: $changed = false;
1302: $newTypes = [];
1303:
1304: foreach ($this->types as $innerType) {
1305: $newKeyType = $cb($innerType->getIterableKeyType(), $right->getIterableKeyType());
1306: $newValueType = $cb($innerType->getIterableValueType(), $right->getIterableValueType());
1307: if ($newKeyType === $innerType->getIterableKeyType() && $newValueType === $innerType->getIterableValueType()) {
1308: $newTypes[] = $innerType;
1309: continue;
1310: }
1311:
1312: $changed = true;
1313: $newTypes[] = TypeTraverser::map($innerType, static function (Type $type, callable $traverse) use ($innerType, $newKeyType, $newValueType): Type {
1314: if ($type === $innerType->getIterableKeyType()) {
1315: return $newKeyType;
1316: }
1317: if ($type === $innerType->getIterableValueType()) {
1318: return $newValueType;
1319: }
1320:
1321: return $traverse($type);
1322: });
1323: }
1324:
1325: if (!$changed) {
1326: return $this;
1327: }
1328:
1329: return TypeCombinator::intersect(...$newTypes);
1330: }
1331:
1332: return $this;
1333: }
1334:
1335: public function tryRemove(Type $typeToRemove): ?Type
1336: {
1337: return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1338: }
1339:
1340: public function exponentiate(Type $exponent): Type
1341: {
1342: return $this->intersectTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1343: }
1344:
1345: public function getFiniteTypes(): array
1346: {
1347: $compare = [];
1348: foreach ($this->types as $type) {
1349: $oneType = [];
1350: foreach ($type->getFiniteTypes() as $finiteType) {
1351: if ($finiteType instanceof EnumCaseObjectType) {
1352: $oneType[$finiteType->getClassName() . '::' . $finiteType->getEnumCaseName()] = $finiteType;
1353: continue;
1354: }
1355: $oneType[$finiteType->describe(VerbosityLevel::typeOnly())] = $finiteType;
1356: }
1357: $compare[] = $oneType;
1358: }
1359:
1360: $result = array_values(array_intersect_key(...$compare));
1361:
1362: if (count($result) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1363: return [];
1364: }
1365:
1366: return $result;
1367: }
1368:
1369: /**
1370: * @param callable(Type $type): TrinaryLogic $getResult
1371: * @param (callable(Type $type): bool)|null $filter
1372: */
1373: private function intersectResults(
1374: callable $getResult,
1375: ?callable $filter = null,
1376: ): TrinaryLogic
1377: {
1378: $types = $this->types;
1379: if ($filter !== null) {
1380: $types = array_filter($types, $filter);
1381: }
1382: if (count($types) === 0) {
1383: return TrinaryLogic::createNo();
1384: }
1385:
1386: return TrinaryLogic::lazyMaxMin($types, $getResult);
1387: }
1388:
1389: /**
1390: * @param callable(Type $type): Type $getType
1391: */
1392: private function intersectTypes(callable $getType): Type
1393: {
1394: $operands = array_map($getType, $this->types);
1395: return TypeCombinator::intersect(...$operands);
1396: }
1397:
1398: public function toPhpDocNode(): TypeNode
1399: {
1400: $baseTypes = [];
1401: $typesToDescribe = [];
1402: $skipTypeNames = [];
1403:
1404: $nonEmptyStr = false;
1405: $nonFalsyStr = false;
1406: $isList = $this->isList()->yes();
1407: $isArray = $this->isArray()->yes();
1408: $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
1409: $describedTypes = [];
1410:
1411: foreach ($this->getSortedTypes() as $i => $type) {
1412: if ($type instanceof AccessoryNonEmptyStringType
1413: || $type instanceof AccessoryLiteralStringType
1414: || $type instanceof AccessoryNumericStringType
1415: || $type instanceof AccessoryNonFalsyStringType
1416: || $type instanceof AccessoryLowercaseStringType
1417: || $type instanceof AccessoryUppercaseStringType
1418: ) {
1419: if ($type instanceof AccessoryNonFalsyStringType) {
1420: $nonFalsyStr = true;
1421: }
1422: if ($type instanceof AccessoryNonEmptyStringType) {
1423: $nonEmptyStr = true;
1424: }
1425: if ($nonEmptyStr && $nonFalsyStr) {
1426: // prevent redundant 'non-empty-string&non-falsy-string'
1427: foreach ($typesToDescribe as $key => $typeToDescribe) {
1428: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
1429: continue;
1430: }
1431:
1432: unset($typesToDescribe[$key]);
1433: }
1434: }
1435:
1436: $typesToDescribe[$i] = $type;
1437: $skipTypeNames[] = 'string';
1438: continue;
1439: }
1440:
1441: if ($isList || $isArray) {
1442: if ($type instanceof ArrayType) {
1443: $keyType = $type->getKeyType();
1444: $valueType = $type->getItemType();
1445: if ($isList) {
1446: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1447: $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-list' : 'list');
1448: if (!$isMixedValueType) {
1449: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1450: $valueType->toPhpDocNode(),
1451: ]);
1452: } else {
1453: $describedTypes[$i] = $identifierTypeNode;
1454: }
1455: } else {
1456: $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
1457: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1458: $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-array' : 'array');
1459: if (!$isMixedKeyType) {
1460: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1461: $keyType->toPhpDocNode(),
1462: $valueType->toPhpDocNode(),
1463: ]);
1464: } elseif (!$isMixedValueType) {
1465: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1466: $valueType->toPhpDocNode(),
1467: ]);
1468: } else {
1469: $describedTypes[$i] = $identifierTypeNode;
1470: }
1471: }
1472: continue;
1473: } elseif ($type instanceof ConstantArrayType) {
1474: $constantArrayTypeNode = $type->toPhpDocNode();
1475: if ($constantArrayTypeNode instanceof ArrayShapeNode) {
1476: $newKind = $constantArrayTypeNode->kind;
1477: if ($isList) {
1478: if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1479: $newKind = ArrayShapeNode::KIND_NON_EMPTY_LIST;
1480: } else {
1481: $newKind = ArrayShapeNode::KIND_LIST;
1482: }
1483: } elseif ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1484: $newKind = ArrayShapeNode::KIND_NON_EMPTY_ARRAY;
1485: }
1486:
1487: if ($newKind !== $constantArrayTypeNode->kind) {
1488: if ($constantArrayTypeNode->sealed) {
1489: $constantArrayTypeNode = ArrayShapeNode::createSealed($constantArrayTypeNode->items, $newKind);
1490: } else {
1491: $constantArrayTypeNode = ArrayShapeNode::createUnsealed($constantArrayTypeNode->items, $constantArrayTypeNode->unsealedType, $newKind);
1492: }
1493: }
1494:
1495: $describedTypes[$i] = $constantArrayTypeNode;
1496: continue;
1497: }
1498: }
1499: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
1500: continue;
1501: }
1502: }
1503:
1504: if (!$type instanceof AccessoryType) {
1505: $baseTypes[$i] = $type;
1506: continue;
1507: }
1508:
1509: $accessoryPhpDocNode = $type->toPhpDocNode();
1510: if ($accessoryPhpDocNode instanceof IdentifierTypeNode && $accessoryPhpDocNode->name === '') {
1511: continue;
1512: }
1513:
1514: $typesToDescribe[$i] = $type;
1515: }
1516:
1517: foreach ($baseTypes as $i => $type) {
1518: $typeNode = $type->toPhpDocNode();
1519: if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') {
1520: $nonEmpty = false;
1521: $typeName = 'array';
1522: foreach ($typesToDescribe as $j => $typeToDescribe) {
1523: if ($typeToDescribe instanceof AccessoryArrayListType) {
1524: $typeName = 'list';
1525: if (count($typeNode->genericTypes) > 1) {
1526: array_shift($typeNode->genericTypes);
1527: }
1528: } elseif ($typeToDescribe instanceof NonEmptyArrayType) {
1529: $nonEmpty = true;
1530: } else {
1531: continue;
1532: }
1533:
1534: unset($typesToDescribe[$j]);
1535: }
1536:
1537: if ($nonEmpty) {
1538: $typeName = 'non-empty-' . $typeName;
1539: }
1540:
1541: $describedTypes[$i] = new GenericTypeNode(
1542: new IdentifierTypeNode($typeName),
1543: $typeNode->genericTypes,
1544: );
1545: continue;
1546: }
1547:
1548: if ($typeNode instanceof IdentifierTypeNode && in_array($typeNode->name, $skipTypeNames, true)) {
1549: continue;
1550: }
1551:
1552: $describedTypes[$i] = $typeNode;
1553: }
1554:
1555: foreach ($typesToDescribe as $i => $typeToDescribe) {
1556: $describedTypes[$i] = $typeToDescribe->toPhpDocNode();
1557: }
1558:
1559: ksort($describedTypes);
1560:
1561: $describedTypes = array_values($describedTypes);
1562:
1563: if (count($describedTypes) === 1) {
1564: return $describedTypes[0];
1565: }
1566:
1567: return new IntersectionTypeNode($describedTypes);
1568: }
1569:
1570: public function hasTemplateOrLateResolvableType(): bool
1571: {
1572: foreach ($this->types as $type) {
1573: if (!$type->hasTemplateOrLateResolvableType()) {
1574: continue;
1575: }
1576:
1577: return true;
1578: }
1579:
1580: return false;
1581: }
1582:
1583: }
1584: