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