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