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\ReflectionProvider;
23: use PHPStan\Reflection\TrivialParametersAcceptor;
24: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection;
25: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection;
26: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
27: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
28: use PHPStan\ShouldNotHappenException;
29: use PHPStan\TrinaryLogic;
30: use PHPStan\Type\Accessory\AccessoryArrayListType;
31: use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
32: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
33: use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
34: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
35: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
36: use PHPStan\Type\Accessory\AccessoryNumericStringType;
37: use PHPStan\Type\Accessory\AccessoryType;
38: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
39: use PHPStan\Type\Accessory\HasOffsetType;
40: use PHPStan\Type\Accessory\HasOffsetValueType;
41: use PHPStan\Type\Accessory\NonEmptyArrayType;
42: use PHPStan\Type\Constant\ConstantArrayType;
43: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
44: use PHPStan\Type\Constant\ConstantIntegerType;
45: use PHPStan\Type\Constant\ConstantStringType;
46: use PHPStan\Type\Enum\EnumCaseObjectType;
47: use PHPStan\Type\Generic\TemplateArrayType;
48: use PHPStan\Type\Generic\TemplateType;
49: use PHPStan\Type\Generic\TemplateTypeMap;
50: use PHPStan\Type\Generic\TemplateTypeVariance;
51: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
52: use PHPStan\Type\Traits\NonRemoveableTypeTrait;
53: use function array_filter;
54: use function array_intersect_key;
55: use function array_map;
56: use function array_shift;
57: use function array_unique;
58: use function array_values;
59: use function count;
60: use function implode;
61: use function in_array;
62: use function is_int;
63: use function ksort;
64: use function sprintf;
65: use function str_starts_with;
66: use function strcasecmp;
67: use function strlen;
68: use function substr;
69: use function usort;
70:
71: /** @api */
72: #[InstanceofDeprecated]
73: class IntersectionType implements CompoundType
74: {
75:
76: use NonRemoveableTypeTrait;
77: use NonGeneralizableTypeTrait;
78:
79: /**
80: * Sorting must not reorder $types: describe() sorts for readability, but $types is the
81: * type's value — getTypes() exposes it and callers merge array shapes in that order. It
82: * used to be sorted in place, so describing a union permanently changed what getTypes()
83: * returned, making an immutable value object's observable state depend on what had been
84: * called on it before.
85: *
86: * @var list<Type>|null
87: */
88: private ?array $sortedTypesCache = null;
89:
90: private ?TrinaryLogic $isBoolean = null;
91:
92: private ?TrinaryLogic $isFloat = null;
93:
94: private ?TrinaryLogic $isInteger = null;
95:
96: private ?TrinaryLogic $isString = null;
97:
98: private ?TrinaryLogic $isArray = null;
99:
100: private ?TrinaryLogic $isList = null;
101:
102: private ?TrinaryLogic $isConstantArray = null;
103:
104: private ?TrinaryLogic $isOversizedArray = null;
105:
106: private ?TrinaryLogic $isOffsetAccessible = null;
107:
108: private ?TrinaryLogic $isIterableAtLeastOnce = null;
109:
110: private ?TrinaryLogic $isConstantScalarValue = null;
111:
112: private ?TrinaryLogic $isCallable = null;
113:
114: /** @var array<string, Type> */
115: private array $cachedGetOffsetValueType = [];
116:
117: /** @var array<string, TrinaryLogic> */
118: private array $cachedHasOffsetValueType = [];
119:
120: /** @var array<int, string> */
121: private array $cachedDescriptions = [];
122:
123: /**
124: * @api
125: * @param list<Type> $types
126: */
127: public function __construct(private array $types)
128: {
129: if (count($types) < 2) {
130: throw new ShouldNotHappenException(sprintf(
131: 'Cannot create %s with: %s',
132: self::class,
133: implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)),
134: ));
135: }
136: }
137:
138: /**
139: * @return list<Type>
140: */
141: public function getTypes(): array
142: {
143: return $this->types;
144: }
145:
146: /**
147: * @return list<Type>
148: */
149: private function getSortedTypes(): array
150: {
151: return $this->sortedTypesCache ??= UnionTypeHelper::sortTypes($this->types);
152: }
153:
154: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
155: {
156: $types = TemplateTypeMap::createEmpty();
157:
158: foreach ($this->types as $type) {
159: $types = $types->intersect($templateType->inferTemplateTypes($type));
160: }
161:
162: return $types;
163: }
164:
165: public function getReferencedClasses(): array
166: {
167: $classes = [];
168: foreach ($this->types as $type) {
169: foreach ($type->getReferencedClasses() as $className) {
170: $classes[] = $className;
171: }
172: }
173:
174: return $classes;
175: }
176:
177: public function getObjectClassNames(): array
178: {
179: $objectClassNames = [];
180: foreach ($this->types as $type) {
181: $innerObjectClassNames = $type->getObjectClassNames();
182: foreach ($innerObjectClassNames as $innerObjectClassName) {
183: $objectClassNames[] = $innerObjectClassName;
184: }
185: }
186:
187: return array_values(array_unique($objectClassNames));
188: }
189:
190: public function getObjectClassReflections(): array
191: {
192: $reflections = [];
193: foreach ($this->types as $type) {
194: foreach ($type->getObjectClassReflections() as $reflection) {
195: $reflections[] = $reflection;
196: }
197: }
198:
199: return $reflections;
200: }
201:
202: public function getArrays(): array
203: {
204: $arrays = [];
205: foreach ($this->types as $type) {
206: foreach ($type->getArrays() as $array) {
207: $arrays[] = $array;
208: }
209: }
210:
211: return $arrays;
212: }
213:
214: public function getConstantArrays(): array
215: {
216: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
217: $builder = ConstantArrayTypeBuilder::createEmpty();
218: $zero = new ConstantIntegerType(0);
219: $builder->setOffsetValueType(
220: $zero,
221: $this->getOffsetValueType($zero),
222: );
223: $one = new ConstantIntegerType(1);
224: $builder->setOffsetValueType(
225: $one,
226: $this->getOffsetValueType($one),
227: );
228: $constantArray = $builder->getArray();
229: if (!$constantArray instanceof ConstantArrayType) {
230: throw new ShouldNotHappenException();
231: }
232:
233: return [$builder->getArray()];
234: }
235:
236: $constantArrays = [];
237: foreach ($this->types as $type) {
238: foreach ($type->getConstantArrays() as $constantArray) {
239: $constantArrays[] = $constantArray;
240: }
241: }
242:
243: return $constantArrays;
244: }
245:
246: public function getConstantStrings(): array
247: {
248: $strings = [];
249: foreach ($this->types as $type) {
250: foreach ($type->getConstantStrings() as $string) {
251: $strings[] = $string;
252: }
253: }
254:
255: return $strings;
256: }
257:
258: public function accepts(Type $otherType, bool $strictTypes): AcceptsResult
259: {
260: $result = AcceptsResult::createYes();
261: foreach ($this->types as $type) {
262: $result = $result->and($type->accepts($otherType, $strictTypes));
263: }
264:
265: if (!$result->yes()) {
266: $isList = $otherType->isList();
267: $reasons = $result->reasons;
268: $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $otherType);
269: if ($this->isList()->yes() && !$isList->yes()) {
270: $reasons[] = sprintf(
271: '%s %s a list.',
272: $otherType->describe($verbosity),
273: $isList->no() ? 'is not' : 'might not be',
274: );
275: }
276:
277: $isNonEmpty = $otherType->isIterableAtLeastOnce();
278: if ($this->isIterableAtLeastOnce()->yes() && !$isNonEmpty->yes()) {
279: $reasons[] = sprintf(
280: '%s %s empty.',
281: $otherType->describe($verbosity),
282: $isNonEmpty->no() ? 'is' : 'might be',
283: );
284: }
285:
286: if (count($reasons) > 0) {
287: return new AcceptsResult($result->result, $reasons);
288: }
289: }
290:
291: return $result;
292: }
293:
294: public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult
295: {
296: if ($otherType instanceof IntersectionType && $this->equals($otherType)) {
297: return IsSuperTypeOfResult::createYes();
298: }
299:
300: if ($otherType instanceof NeverType) {
301: return IsSuperTypeOfResult::createYes();
302: }
303:
304: return IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types));
305: }
306:
307: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
308: {
309: if (($otherType instanceof self || $otherType instanceof UnionType) && !$otherType instanceof TemplateType) {
310: return $otherType->isSuperTypeOf($this);
311: }
312:
313: $result = IsSuperTypeOfResult::lazyMaxMin(
314: $this->types,
315: static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType),
316: );
317:
318: if (
319: !$result->no()
320: && $this->isOversizedArray()->yes()
321: && !$otherType->isIterableAtLeastOnce()->no()
322: ) {
323: return IsSuperTypeOfResult::createYes();
324: }
325:
326: return $result;
327: }
328:
329: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
330: {
331: $result = AcceptsResult::lazyMaxMin(
332: $this->types,
333: static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes),
334: );
335:
336: // lazyMaxMin can short-circuit to Yes when array<mixed> (inside e.g. array&callable
337: // or array&hasOffsetValue) is accepted by a specific array type like array<int>,
338: // because MixedType::isAcceptedBy() always returns Yes. The isSuperTypeOf check
339: // considers the intersection holistically and catches these false positives.
340: if ($result->yes()) {
341: $isSuperType = $acceptingType->isSuperTypeOf($this);
342: if ($isSuperType->no()) {
343: return $isSuperType->toAcceptsResult();
344: }
345:
346: // A TemplateType member accepts eagerly, so lazyMaxMin's Yes may come solely from it.
347: // A Maybe from the holistic isSuperTypeOf means no member is a definite subtype, so
348: // when a TemplateType member forced the eager Yes, that Yes is untrustworthy - trust the Maybe.
349: // Only distrust when a TemplateType member itself accepts with Yes; if it accepts with Maybe,
350: // the eager Yes came from a non-template member and reflects a genuine (object-level) match.
351: if ($isSuperType->maybe()) {
352: foreach ($this->types as $innerType) {
353: if ($innerType instanceof TemplateType && $acceptingType->accepts($innerType, $strictTypes)->yes()) {
354: return $isSuperType->toAcceptsResult();
355: }
356: }
357: }
358: }
359:
360: if ($this->isOversizedArray()->yes()) {
361: if (!$result->no()) {
362: return AcceptsResult::createYes();
363: }
364: }
365:
366: return $result;
367: }
368:
369: public function equals(Type $type): bool
370: {
371: if (!$type instanceof static) {
372: return false;
373: }
374:
375: if (count($this->types) !== count($type->types)) {
376: return false;
377: }
378:
379: $otherTypes = $type->types;
380: foreach ($this->types as $innerType) {
381: $match = false;
382: foreach ($otherTypes as $i => $otherType) {
383: if (!$innerType->equals($otherType)) {
384: continue;
385: }
386:
387: $match = true;
388: unset($otherTypes[$i]);
389: break;
390: }
391:
392: if (!$match) {
393: return false;
394: }
395: }
396:
397: return count($otherTypes) === 0;
398: }
399:
400: public function describe(VerbosityLevel $level): string
401: {
402: if (isset($this->cachedDescriptions[$level->getLevelValue()])) {
403: return $this->cachedDescriptions[$level->getLevelValue()];
404: }
405:
406: return $this->cachedDescriptions[$level->getLevelValue()] = $level->handle(
407: fn (): string => $this->describeType($level),
408: fn (): string => $this->describeItself($level, true),
409: fn (): string => $this->describeItself($level, false),
410: );
411: }
412:
413: private function describeType(VerbosityLevel $level): string
414: {
415: $typeNames = [];
416: $isList = $this->isList()->yes();
417: $valueType = null;
418: foreach ($this->getSortedTypes() as $type) {
419: if ($isList) {
420: if ($type instanceof ArrayType || $type instanceof ConstantArrayType) {
421: $valueType = $type->getIterableValueType();
422: continue;
423: }
424: if ($type instanceof NonEmptyArrayType) {
425: continue;
426: }
427: }
428: if ($type instanceof AccessoryType) {
429: continue;
430: }
431: $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level);
432: }
433:
434: if ($isList) {
435: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
436: $innerType = '';
437: if ($valueType !== null && !$isMixedValueType) {
438: $innerType = sprintf('<%s>', $valueType->describe($level));
439: }
440:
441: $typeNames[] = 'list' . $innerType;
442: }
443:
444: usort($typeNames, static function ($a, $b) {
445: $cmp = strcasecmp($a, $b);
446: if ($cmp !== 0) {
447: return $cmp;
448: }
449:
450: return $a <=> $b;
451: });
452:
453: return implode('&', $typeNames);
454: }
455:
456: private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string
457: {
458: $baseTypes = [];
459: $typesToDescribe = [];
460: $skipTypeNames = [];
461:
462: $nonEmptyStr = false;
463: $nonFalsyStr = false;
464: $isList = $this->isList()->yes();
465: $isArray = $this->isArray()->yes();
466: $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
467: // When a TemplateArrayType carries the array refinement, we describe
468: // it via its own describe() (e.g. "T of array") rather than collapsing
469: // it into a generic `array<...>` prefix. In that case the
470: // `NonEmptyArrayType` and `AccessoryArrayListType` markers must
471: // describe themselves explicitly — they cannot be absorbed into a
472: // non-existent `non-empty-array` prefix.
473: $hasTemplateArray = false;
474: if ($isArray || $isList) {
475: foreach ($this->types as $type) {
476: if ($type instanceof TemplateArrayType) {
477: $hasTemplateArray = true;
478: break;
479: }
480: }
481: }
482: $describedTypes = [];
483: foreach ($this->getSortedTypes() as $i => $type) {
484: if ($type instanceof AccessoryNonEmptyStringType
485: || $type instanceof AccessoryLiteralStringType
486: || $type instanceof AccessoryNumericStringType
487: || $type instanceof AccessoryNonFalsyStringType
488: || $type instanceof AccessoryLowercaseStringType
489: || $type instanceof AccessoryUppercaseStringType
490: || $type instanceof AccessoryDecimalIntegerStringType
491: ) {
492: if (
493: ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType)
494: && !$level->isPrecise()
495: && !$level->isCache()
496: ) {
497: continue;
498: }
499: if ($type instanceof AccessoryNonFalsyStringType) {
500: $nonFalsyStr = true;
501: }
502: if ($type instanceof AccessoryNonEmptyStringType) {
503: $nonEmptyStr = true;
504: }
505: if ($nonEmptyStr && $nonFalsyStr) {
506: // prevent redundant 'non-empty-string&non-falsy-string'
507: foreach ($typesToDescribe as $key => $typeToDescribe) {
508: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
509: continue;
510: }
511:
512: unset($typesToDescribe[$key]);
513: }
514: }
515:
516: $typesToDescribe[$i] = $type;
517: $skipTypeNames[] = 'string';
518: continue;
519: }
520: if ($isList || $isArray) {
521: if ($type instanceof TemplateArrayType) {
522: // Preserve the template's own describe (e.g. "T of array")
523: // instead of collapsing it to a generic array shape — the
524: // other intersection members already carry the array
525: // refinement.
526: $describedTypes[$i] = $type->describe($level);
527: continue;
528: }
529: if ($type instanceof ArrayType) {
530: $keyType = $type->getKeyType();
531: $valueType = $type->getItemType();
532: if ($isList) {
533: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
534: $valueTypeDescription = '';
535: if (!$isMixedValueType) {
536: $valueTypeDescription = sprintf('<%s>', $valueType->describe($level));
537: }
538:
539: $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-list' : 'list') . $valueTypeDescription;
540: } else {
541: $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
542: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
543: $typeDescription = '';
544: if (!$isMixedKeyType) {
545: $typeDescription = sprintf('<%s, %s>', $keyType->describe($level), $valueType->describe($level));
546: } elseif (!$isMixedValueType) {
547: $typeDescription = sprintf('<%s>', $valueType->describe($level));
548: }
549:
550: $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-array' : 'array') . $typeDescription;
551: }
552: continue;
553: } elseif ($type instanceof ConstantArrayType) {
554: $description = $type->describe($level);
555: $kind = str_starts_with($description, 'list') ? 'list' : 'array';
556: $descriptionWithoutKind = substr($description, strlen($kind));
557: $begin = $isList ? 'list' : 'array';
558: if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
559: $begin = 'non-empty-' . $begin;
560: }
561:
562: $describedTypes[$i] = $begin . $descriptionWithoutKind;
563: continue;
564: }
565: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
566: if ($hasTemplateArray) {
567: $describedTypes[$i] = $type->describe($level);
568: }
569: continue;
570: }
571: }
572:
573: if ($type instanceof CallableType && $type->isCommonCallable()) {
574: $typesToDescribe[$i] = $type;
575: $skipTypeNames[] = 'object';
576: $skipTypeNames[] = 'string';
577: continue;
578: }
579:
580: if (!$type instanceof AccessoryType) {
581: $baseTypes[$i] = $type;
582: continue;
583: }
584:
585: if ($skipAccessoryTypes) {
586: continue;
587: }
588:
589: $typesToDescribe[$i] = $type;
590: }
591:
592: foreach ($baseTypes as $i => $type) {
593: $typeDescription = $type->describe($level);
594:
595: if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) {
596: foreach ($typesToDescribe as $j => $typeToDescribe) {
597: if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) {
598: $describedTypes[$i] = 'callable-' . $typeDescription;
599: unset($typesToDescribe[$j]);
600: continue 2;
601: }
602: }
603: }
604:
605: if (in_array($typeDescription, $skipTypeNames, true)) {
606: continue;
607: }
608:
609: $describedTypes[$i] = $type->describe($level);
610: }
611:
612: foreach ($typesToDescribe as $i => $typeToDescribe) {
613: $describedTypes[$i] = $typeToDescribe->describe($level);
614: }
615:
616: ksort($describedTypes);
617:
618: return implode('&', $describedTypes);
619: }
620:
621: public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type
622: {
623: return $this->intersectTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName));
624: }
625:
626: public function isObject(): TrinaryLogic
627: {
628: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isObject());
629: }
630:
631: public function getClassStringType(): Type
632: {
633: return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringType());
634: }
635:
636: public function isEnum(): TrinaryLogic
637: {
638: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isEnum());
639: }
640:
641: public function canAccessProperties(): TrinaryLogic
642: {
643: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
644: }
645:
646: public function hasProperty(string $propertyName): TrinaryLogic
647: {
648: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
649: }
650:
651: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
652: {
653: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
654: }
655:
656: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
657: {
658: $propertyPrototypes = [];
659: foreach ($this->types as $type) {
660: if (!$type->hasProperty($propertyName)->yes()) {
661: continue;
662: }
663:
664: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
665: }
666:
667: $propertiesCount = count($propertyPrototypes);
668: if ($propertiesCount === 0) {
669: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
670: }
671:
672: if ($propertiesCount === 1) {
673: return $propertyPrototypes[0];
674: }
675:
676: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
677: }
678:
679: public function hasInstanceProperty(string $propertyName): TrinaryLogic
680: {
681: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName));
682: }
683:
684: public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
685: {
686: return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty();
687: }
688:
689: public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
690: {
691: $propertyPrototypes = [];
692: foreach ($this->types as $type) {
693: if (!$type->hasInstanceProperty($propertyName)->yes()) {
694: continue;
695: }
696:
697: $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this);
698: }
699:
700: $propertiesCount = count($propertyPrototypes);
701: if ($propertiesCount === 0) {
702: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
703: }
704:
705: if ($propertiesCount === 1) {
706: return $propertyPrototypes[0];
707: }
708:
709: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
710: }
711:
712: public function hasStaticProperty(string $propertyName): TrinaryLogic
713: {
714: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName));
715: }
716:
717: public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
718: {
719: return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty();
720: }
721:
722: public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
723: {
724: $propertyPrototypes = [];
725: foreach ($this->types as $type) {
726: if (!$type->hasStaticProperty($propertyName)->yes()) {
727: continue;
728: }
729:
730: $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
731: }
732:
733: $propertiesCount = count($propertyPrototypes);
734: if ($propertiesCount === 0) {
735: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
736: }
737:
738: if ($propertiesCount === 1) {
739: return $propertyPrototypes[0];
740: }
741:
742: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
743: }
744:
745: public function canCallMethods(): TrinaryLogic
746: {
747: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
748: }
749:
750: public function hasMethod(string $methodName): TrinaryLogic
751: {
752: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
753: }
754:
755: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
756: {
757: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
758: }
759:
760: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
761: {
762: $methodPrototypes = [];
763: foreach ($this->types as $type) {
764: if (!$type->hasMethod($methodName)->yes()) {
765: continue;
766: }
767:
768: $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this);
769: }
770:
771: $methodsCount = count($methodPrototypes);
772: if ($methodsCount === 0) {
773: throw new MissingMethodFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $methodName);
774: }
775:
776: if ($methodsCount === 1) {
777: return $methodPrototypes[0];
778: }
779:
780: return new IntersectionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
781: }
782:
783: public function canAccessConstants(): TrinaryLogic
784: {
785: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
786: }
787:
788: public function hasConstant(string $constantName): TrinaryLogic
789: {
790: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName));
791: }
792:
793: public function getConstant(string $constantName): ClassConstantReflection
794: {
795: foreach ($this->types as $type) {
796: if ($type->hasConstant($constantName)->yes()) {
797: return $type->getConstant($constantName);
798: }
799: }
800:
801: throw new MissingConstantFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $constantName);
802: }
803:
804: public function isIterable(): TrinaryLogic
805: {
806: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
807: }
808:
809: public function isIterableAtLeastOnce(): TrinaryLogic
810: {
811: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
812: return TrinaryLogic::createYes();
813: }
814:
815: return $this->isIterableAtLeastOnce ??= $this->intersectResults(
816: static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce(),
817: static fn (Type $type): bool => !$type->isIterable()->no(),
818: );
819: }
820:
821: public function getArraySize(): Type
822: {
823: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
824: return new ConstantIntegerType(2);
825: }
826:
827: $arraySize = $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize());
828:
829: $knownOffsets = [];
830: foreach ($this->types as $type) {
831: if (!($type instanceof HasOffsetValueType) && !($type instanceof HasOffsetType)) {
832: continue;
833: }
834:
835: $knownOffsets[$type->getOffsetType()->getValue()] = true;
836: }
837:
838: if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) {
839: $knownOffsets[0] = true;
840: }
841:
842: if ($knownOffsets !== []) {
843: return TypeCombinator::intersect($arraySize, IntegerRangeType::fromInterval(count($knownOffsets), null));
844: }
845:
846: return $arraySize;
847: }
848:
849: public function getIterableKeyType(): Type
850: {
851: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
852: return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
853: }
854: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
855: }
856:
857: public function getFirstIterableKeyType(): Type
858: {
859: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
860: }
861:
862: public function getLastIterableKeyType(): Type
863: {
864: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
865: }
866:
867: public function getIterableValueType(): Type
868: {
869: $result = $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
870: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
871: return TypeCombinator::intersect(
872: $result,
873: new UnionType([
874: new ObjectWithoutClassType(),
875: new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]),
876: ]),
877: );
878: }
879: return $result;
880: }
881:
882: public function getFirstIterableValueType(): Type
883: {
884: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
885: }
886:
887: public function getLastIterableValueType(): Type
888: {
889: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
890: }
891:
892: public function isArray(): TrinaryLogic
893: {
894: return $this->isArray ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray());
895: }
896:
897: public function isConstantArray(): TrinaryLogic
898: {
899: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
900: return TrinaryLogic::createYes();
901: }
902: return $this->isConstantArray ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray());
903: }
904:
905: public function isOversizedArray(): TrinaryLogic
906: {
907: return $this->isOversizedArray ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
908: }
909:
910: public function isList(): TrinaryLogic
911: {
912: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
913: return TrinaryLogic::createYes();
914: }
915:
916: return $this->isList ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isList());
917: }
918:
919: public function isString(): TrinaryLogic
920: {
921: return $this->isString ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString());
922: }
923:
924: public function isNumericString(): TrinaryLogic
925: {
926: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
927: }
928:
929: public function isDecimalIntegerString(): TrinaryLogic
930: {
931: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isDecimalIntegerString());
932: }
933:
934: public function isNonEmptyString(): TrinaryLogic
935: {
936: if ($this->isCallable()->yes() && $this->isString()->yes()) {
937: return TrinaryLogic::createYes();
938: }
939: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
940: }
941:
942: public function isNonFalsyString(): TrinaryLogic
943: {
944: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
945: }
946:
947: public function isLiteralString(): TrinaryLogic
948: {
949: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
950: }
951:
952: public function isLowercaseString(): TrinaryLogic
953: {
954: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString());
955: }
956:
957: public function isUppercaseString(): TrinaryLogic
958: {
959: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString());
960: }
961:
962: public function isClassString(): TrinaryLogic
963: {
964: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassString());
965: }
966:
967: public function getClassStringObjectType(): Type
968: {
969: return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringObjectType());
970: }
971:
972: public function getObjectTypeOrClassStringObjectType(): Type
973: {
974: return $this->intersectTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType());
975: }
976:
977: public function isVoid(): TrinaryLogic
978: {
979: return TrinaryLogic::createNo();
980: }
981:
982: public function isScalar(): TrinaryLogic
983: {
984: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isScalar());
985: }
986:
987: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
988: {
989: return $this->intersectResults(
990: static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic(),
991: )->toBooleanType();
992: }
993:
994: public function isOffsetAccessible(): TrinaryLogic
995: {
996: return $this->isOffsetAccessible ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
997: }
998:
999: public function isOffsetAccessLegal(): TrinaryLogic
1000: {
1001: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
1002: }
1003:
1004: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
1005: {
1006: $cacheKey = $offsetType->describe(VerbosityLevel::cache());
1007: if (isset($this->cachedHasOffsetValueType[$cacheKey])) {
1008: return $this->cachedHasOffsetValueType[$cacheKey];
1009: }
1010: return $this->cachedHasOffsetValueType[$cacheKey] = $this->doHasOffsetValueType($offsetType);
1011: }
1012:
1013: private function doHasOffsetValueType(Type $offsetType): TrinaryLogic
1014: {
1015: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
1016: $arrayKeyOffsetType = $offsetType->toArrayKey();
1017: $callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
1018:
1019: return $callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->result;
1020: }
1021:
1022: if ($this->isList()->yes()) {
1023: $arrayKeyOffsetType = $offsetType->toArrayKey();
1024:
1025: $negative = IntegerRangeType::fromInterval(null, -1);
1026: if ($negative->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1027: return TrinaryLogic::createNo();
1028: }
1029:
1030: $size = $this->getArraySize();
1031: if ($size instanceof IntegerRangeType && $size->getMin() !== null) {
1032: $knownOffsets = IntegerRangeType::fromInterval(0, $size->getMin() - 1);
1033: } elseif ($size instanceof ConstantIntegerType) {
1034: $knownOffsets = IntegerRangeType::fromInterval(0, $size->getValue() - 1);
1035: } elseif ($this->isIterableAtLeastOnce()->yes()) {
1036: $knownOffsets = new ConstantIntegerType(0);
1037: } else {
1038: $knownOffsets = null;
1039: }
1040:
1041: if ($knownOffsets !== null && $knownOffsets->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1042: return TrinaryLogic::createYes();
1043: }
1044:
1045: foreach ($this->types as $type) {
1046: if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
1047: continue;
1048: }
1049:
1050: foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
1051: if (!is_int($constantScalarValue)) {
1052: continue;
1053: }
1054: if (IntegerRangeType::fromInterval(0, $constantScalarValue)->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1055: return TrinaryLogic::createYes();
1056: }
1057: }
1058: }
1059: }
1060:
1061: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
1062: }
1063:
1064: public function getOffsetValueType(Type $offsetType): Type
1065: {
1066: $cacheKey = $offsetType->describe(VerbosityLevel::cache());
1067: if (isset($this->cachedGetOffsetValueType[$cacheKey])) {
1068: return $this->cachedGetOffsetValueType[$cacheKey];
1069: }
1070: return $this->cachedGetOffsetValueType[$cacheKey] = $this->doGetOffsetValueType($offsetType);
1071: }
1072:
1073: private function doGetOffsetValueType(Type $offsetType): Type
1074: {
1075: $result = $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType));
1076: if ($this->isOversizedArray()->yes()) {
1077: return TypeUtils::toBenevolentUnion($result);
1078: }
1079:
1080: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
1081: $arrayKeyOffsetType = $offsetType->toArrayKey();
1082: if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1083: $narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]);
1084: } elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1085: $narrowedType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]);
1086: } else {
1087: $narrowedType = new UnionType([new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), new ObjectWithoutClassType()]);
1088: }
1089: $result = TypeCombinator::intersect($result, $narrowedType);
1090: }
1091:
1092: return $result;
1093: }
1094:
1095: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
1096: {
1097: if ($this->isOversizedArray()->yes()) {
1098: return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type {
1099: // avoid new HasOffsetValueType being intersected with oversized array
1100: if (!$type instanceof ArrayType) {
1101: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
1102: }
1103:
1104: if (!$offsetType instanceof ConstantStringType && !$offsetType instanceof ConstantIntegerType) {
1105: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
1106: }
1107:
1108: if (!$offsetType->isSuperTypeOf($type->getKeyType())->yes()) {
1109: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
1110: }
1111:
1112: return new IntersectionType([
1113: new ArrayType(
1114: TypeCombinator::union($type->getKeyType(), $offsetType),
1115: TypeCombinator::union($type->getItemType(), $valueType),
1116: ),
1117: new NonEmptyArrayType(),
1118: ]);
1119: });
1120: }
1121:
1122: $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
1123:
1124: if (
1125: $offsetType !== null
1126: && $this->isList()->yes()
1127: && !$result->isList()->yes()
1128: ) {
1129: if ($this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) {
1130: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
1131: } else {
1132: foreach ($this->types as $type) {
1133: if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
1134: continue;
1135: }
1136:
1137: foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
1138: if (!is_int($constantScalarValue)) {
1139: continue;
1140: }
1141: if (IntegerRangeType::fromInterval(0, $constantScalarValue + 1)->isSuperTypeOf($offsetType)->yes()) {
1142: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
1143: break 2;
1144: }
1145: }
1146: }
1147: }
1148: }
1149:
1150: if (
1151: $this->isList()->yes()
1152: && $offsetType !== null
1153: && $offsetType->toArrayKey()->isInteger()->yes()
1154: && $this->getIterableValueType()->isArray()->yes()
1155: ) {
1156: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
1157: }
1158:
1159: return $result;
1160: }
1161:
1162: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
1163: {
1164: return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
1165: }
1166:
1167: public function unsetOffset(Type $offsetType): Type
1168: {
1169: return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
1170: }
1171:
1172: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
1173: {
1174: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict));
1175: }
1176:
1177: public function getKeysArray(): Type
1178: {
1179: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray());
1180: }
1181:
1182: public function getValuesArray(): Type
1183: {
1184: $cb = static fn (Type $type): Type => $type->getValuesArray();
1185: if ($this->isList()->yes()) {
1186: return $this;
1187: }
1188: return $this->intersectTypes($cb);
1189: }
1190:
1191: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
1192: {
1193: return $this->intersectTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
1194: }
1195:
1196: public function fillKeysArray(Type $valueType): Type
1197: {
1198: return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
1199: }
1200:
1201: public function flipArray(): Type
1202: {
1203: return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray());
1204: }
1205:
1206: public function intersectKeyArray(Type $otherArraysType): Type
1207: {
1208: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
1209: }
1210:
1211: public function popArray(): Type
1212: {
1213: if ($this->isList()->yes()) {
1214: // hasOffsetValue(n, T) on a list proves indices 0..n exist; popping
1215: // removes the highest index, so offsets up to n - 1 survive. Their
1216: // values are unknown - the value known at n may have been the popped one.
1217: $members = [];
1218: foreach ($this->types as $type) {
1219: if ($type instanceof TemplateType) {
1220: $members[] = $type;
1221: continue;
1222: }
1223: if ($type instanceof HasOffsetValueType || $type instanceof HasOffsetType) {
1224: $offsetType = $type->getOffsetType();
1225: if ($offsetType instanceof ConstantIntegerType && $offsetType->getValue() >= 1) {
1226: $members[] = new HasOffsetType(new ConstantIntegerType($offsetType->getValue() - 1));
1227: }
1228: continue;
1229: }
1230: $members[] = $type->popArray();
1231: }
1232:
1233: return TypeCombinator::intersect(...$members);
1234: }
1235:
1236: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->popArray());
1237: }
1238:
1239: public function reverseArray(TrinaryLogic $preserveKeys): Type
1240: {
1241: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->reverseArray($preserveKeys));
1242: }
1243:
1244: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
1245: {
1246: return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict));
1247: }
1248:
1249: public function shiftArray(): Type
1250: {
1251: if ($this->isList()->yes()) {
1252: // shifting a list reindexes: index n's value always moves to n - 1
1253: $members = [];
1254: foreach ($this->types as $type) {
1255: if ($type instanceof TemplateType) {
1256: $members[] = $type;
1257: continue;
1258: }
1259: if ($type instanceof HasOffsetValueType) {
1260: $offsetType = $type->getOffsetType();
1261: if ($offsetType instanceof ConstantIntegerType && $offsetType->getValue() >= 1) {
1262: $members[] = new HasOffsetValueType(new ConstantIntegerType($offsetType->getValue() - 1), $type->getValueType());
1263: }
1264: continue;
1265: }
1266: if ($type instanceof HasOffsetType) {
1267: $offsetType = $type->getOffsetType();
1268: if ($offsetType instanceof ConstantIntegerType && $offsetType->getValue() >= 1) {
1269: $members[] = new HasOffsetType(new ConstantIntegerType($offsetType->getValue() - 1));
1270: }
1271: continue;
1272: }
1273: $members[] = $type->shiftArray();
1274: }
1275:
1276: return TypeCombinator::intersect(...$members);
1277: }
1278:
1279: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->shiftArray());
1280: }
1281:
1282: public function shuffleArray(): Type
1283: {
1284: $cb = static fn (Type $type): Type => $type->shuffleArray();
1285: if ($this->isList()->yes()) {
1286: return $this->intersectTypesPreserveTemplateType($cb);
1287: }
1288: return $this->intersectTypes($cb);
1289: }
1290:
1291: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
1292: {
1293: $result = $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
1294:
1295: if (
1296: $this->isList()->yes()
1297: && $this->isIterableAtLeastOnce()->yes()
1298: && (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()
1299: && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()
1300: ) {
1301: $result = TypeCombinator::intersect($result, new NonEmptyArrayType());
1302: }
1303:
1304: return $result;
1305: }
1306:
1307: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1308: {
1309: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
1310: }
1311:
1312: public function truncateListToSize(Type $sizeType): Type
1313: {
1314: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->truncateListToSize($sizeType));
1315: }
1316:
1317: public function makeListMaybe(): Type
1318: {
1319: return $this->intersectTypes(static fn (Type $type): Type => $type->makeListMaybe());
1320: }
1321:
1322: public function mapValueType(callable $cb): Type
1323: {
1324: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->mapValueType($cb));
1325: }
1326:
1327: public function mapKeyType(callable $cb): Type
1328: {
1329: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->mapKeyType($cb));
1330: }
1331:
1332: public function makeAllArrayKeysOptional(): Type
1333: {
1334: return $this->intersectTypes(static fn (Type $type): Type => $type->makeAllArrayKeysOptional());
1335: }
1336:
1337: public function changeKeyCaseArray(?int $case): Type
1338: {
1339: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->changeKeyCaseArray($case));
1340: }
1341:
1342: public function filterArrayRemovingFalsey(): Type
1343: {
1344: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->filterArrayRemovingFalsey());
1345: }
1346:
1347: public function getEnumCases(): array
1348: {
1349: $compare = [];
1350: foreach ($this->types as $type) {
1351: $oneType = [];
1352: foreach ($type->getEnumCases() as $enumCase) {
1353: $oneType[$enumCase->getClassName() . '::' . $enumCase->getEnumCaseName()] = $enumCase;
1354: }
1355: $compare[] = $oneType;
1356: }
1357:
1358: return array_values(array_intersect_key(...$compare));
1359: }
1360:
1361: public function getEnumCaseObject(): ?EnumCaseObjectType
1362: {
1363: $singleCase = null;
1364: foreach ($this->types as $type) {
1365: $caseObject = $type->getEnumCaseObject();
1366: if ($caseObject === null) {
1367: continue;
1368: }
1369:
1370: if ($singleCase !== null) {
1371: return null;
1372: }
1373:
1374: $singleCase = $caseObject;
1375: }
1376:
1377: return $singleCase;
1378: }
1379:
1380: public function isCallable(): TrinaryLogic
1381: {
1382: return $this->isCallable ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
1383: }
1384:
1385: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
1386: {
1387: $yesAcceptors = [];
1388:
1389: foreach ($this->types as $type) {
1390: if (!$type->isCallable()->yes()) {
1391: continue;
1392: }
1393: $yesAcceptors[] = $type->getCallableParametersAcceptors($scope);
1394: }
1395:
1396: if (count($yesAcceptors) === 0) {
1397: if ($this->isCallable()->no()) {
1398: throw new ShouldNotHappenException();
1399: }
1400:
1401: return [new TrivialParametersAcceptor()];
1402: }
1403:
1404: $result = [];
1405: $combinations = CombinationsHelper::combinations($yesAcceptors);
1406: foreach ($combinations as $combination) {
1407: $combined = ParametersAcceptorSelector::combineAcceptors($combination);
1408: if (!$combined instanceof CallableParametersAcceptor) {
1409: throw new ShouldNotHappenException();
1410: }
1411: $result[] = $combined;
1412: }
1413:
1414: return $result;
1415: }
1416:
1417: public function isCloneable(): TrinaryLogic
1418: {
1419: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
1420: }
1421:
1422: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1423: {
1424: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion));
1425: }
1426:
1427: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1428: {
1429: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion));
1430: }
1431:
1432: public function isNull(): TrinaryLogic
1433: {
1434: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNull());
1435: }
1436:
1437: public function isConstantValue(): TrinaryLogic
1438: {
1439: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
1440: }
1441:
1442: public function isConstantScalarValue(): TrinaryLogic
1443: {
1444: return $this->isConstantScalarValue ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
1445: }
1446:
1447: public function getConstantScalarTypes(): array
1448: {
1449: $scalarTypes = [];
1450: foreach ($this->types as $type) {
1451: foreach ($type->getConstantScalarTypes() as $scalarType) {
1452: $scalarTypes[] = $scalarType;
1453: }
1454: }
1455:
1456: return $scalarTypes;
1457: }
1458:
1459: public function getConstantScalarValues(): array
1460: {
1461: $values = [];
1462: foreach ($this->types as $type) {
1463: foreach ($type->getConstantScalarValues() as $value) {
1464: $values[] = $value;
1465: }
1466: }
1467:
1468: return $values;
1469: }
1470:
1471: public function isTrue(): TrinaryLogic
1472: {
1473: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
1474: }
1475:
1476: public function isFalse(): TrinaryLogic
1477: {
1478: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
1479: }
1480:
1481: public function isBoolean(): TrinaryLogic
1482: {
1483: return $this->isBoolean ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
1484: }
1485:
1486: public function isFloat(): TrinaryLogic
1487: {
1488: return $this->isFloat ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
1489: }
1490:
1491: public function isInteger(): TrinaryLogic
1492: {
1493: return $this->isInteger ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
1494: }
1495:
1496: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1497: {
1498: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion));
1499: }
1500:
1501: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1502: {
1503: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion));
1504: }
1505:
1506: public function getSmallerType(PhpVersion $phpVersion): Type
1507: {
1508: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion));
1509: }
1510:
1511: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
1512: {
1513: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion));
1514: }
1515:
1516: public function getGreaterType(PhpVersion $phpVersion): Type
1517: {
1518: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion));
1519: }
1520:
1521: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
1522: {
1523: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion));
1524: }
1525:
1526: public function toBoolean(): BooleanType
1527: {
1528: $type = $this->intersectTypes(static fn (Type $type): BooleanType => $type->toBoolean());
1529:
1530: if (!$type instanceof BooleanType) {
1531: return new BooleanType();
1532: }
1533:
1534: return $type;
1535: }
1536:
1537: public function toNumber(): Type
1538: {
1539: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toNumber());
1540:
1541: return $type;
1542: }
1543:
1544: public function toBitwiseNotType(): Type
1545: {
1546: return $this->intersectTypes(static fn (Type $type): Type => $type->toBitwiseNotType());
1547: }
1548:
1549: public function toGetClassResultType(): Type
1550: {
1551: return $this->intersectTypes(static fn (Type $type): Type => $type->toGetClassResultType());
1552: }
1553:
1554: public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
1555: {
1556: return $this->intersectTypes(static fn (Type $type): Type => $type->toClassConstantType($reflectionProvider));
1557: }
1558:
1559: public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
1560: {
1561: $types = [];
1562: $uncertainty = false;
1563: foreach ($this->getTypes() as $innerType) {
1564: $result = $innerType->toObjectTypeForInstanceofCheck();
1565: $types[] = $result->type;
1566: if (!$result->uncertainty) {
1567: continue;
1568: }
1569:
1570: $uncertainty = true;
1571: }
1572:
1573: return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty);
1574: }
1575:
1576: public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
1577: {
1578: $types = [];
1579: $uncertainty = false;
1580: foreach ($this->getTypes() as $innerType) {
1581: $result = $innerType->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass);
1582: $types[] = $result->type;
1583: if (!$result->uncertainty) {
1584: continue;
1585: }
1586:
1587: $uncertainty = true;
1588: }
1589:
1590: return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty);
1591: }
1592:
1593: public function toAbsoluteNumber(): Type
1594: {
1595: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());
1596:
1597: return $type;
1598: }
1599:
1600: public function toString(): Type
1601: {
1602: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString());
1603:
1604: return $type;
1605: }
1606:
1607: public function toInteger(): Type
1608: {
1609: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toInteger());
1610:
1611: return $type;
1612: }
1613:
1614: public function toFloat(): Type
1615: {
1616: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toFloat());
1617:
1618: return $type;
1619: }
1620:
1621: public function toArray(): Type
1622: {
1623: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toArray());
1624:
1625: return $type;
1626: }
1627:
1628: public function toArrayKey(): Type
1629: {
1630: if ($this->isDecimalIntegerString()->yes()) {
1631: return new IntegerType();
1632: }
1633:
1634: if ($this->isNumericString()->yes()) {
1635: return TypeCombinator::union(
1636: new IntegerType(),
1637: $this,
1638: );
1639: }
1640:
1641: if ($this->isString()->yes()) {
1642: return $this;
1643: }
1644:
1645: return $this->intersectTypes(static fn (Type $type): Type => $type->toArrayKey());
1646: }
1647:
1648: public function toCoercedArgumentType(bool $strictTypes): Type
1649: {
1650: return $this->intersectTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes));
1651: }
1652:
1653: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1654: {
1655: $types = TemplateTypeMap::createEmpty();
1656:
1657: foreach ($this->types as $type) {
1658: $types = $types->intersect($type->inferTemplateTypes($receivedType));
1659: }
1660:
1661: return $types;
1662: }
1663:
1664: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1665: {
1666: $references = [];
1667:
1668: foreach ($this->types as $type) {
1669: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
1670: $references[] = $reference;
1671: }
1672: }
1673:
1674: return $references;
1675: }
1676:
1677: public function traverse(callable $cb): Type
1678: {
1679: $types = [];
1680: $changed = false;
1681:
1682: foreach ($this->types as $type) {
1683: $newType = $cb($type);
1684: if ($type !== $newType) {
1685: $changed = true;
1686: }
1687: $types[] = $newType;
1688: }
1689:
1690: if ($changed) {
1691: $result = $types[0];
1692: for ($i = 1, $count = count($types); $i < $count; $i++) {
1693: $result = TypeCombinator::intersect($result, $types[$i]);
1694: }
1695: return $result;
1696: }
1697:
1698: return $this;
1699: }
1700:
1701: public function traverseSimultaneously(Type $right, callable $cb): Type
1702: {
1703: if ($this->isArray()->yes() && $right->isArray()->yes()) {
1704: $changed = false;
1705: $newTypes = [];
1706:
1707: foreach ($this->types as $innerType) {
1708: $newKeyType = $cb($innerType->getIterableKeyType(), $right->getIterableKeyType());
1709: $newValueType = $cb($innerType->getIterableValueType(), $right->getIterableValueType());
1710: if ($newKeyType === $innerType->getIterableKeyType() && $newValueType === $innerType->getIterableValueType()) {
1711: $newTypes[] = $innerType;
1712: continue;
1713: }
1714:
1715: $changed = true;
1716: $newTypes[] = TypeTraverser::map($innerType, static function (Type $type, callable $traverse) use ($innerType, $newKeyType, $newValueType): Type {
1717: if ($type === $innerType->getIterableKeyType()) {
1718: return $newKeyType;
1719: }
1720: if ($type === $innerType->getIterableValueType()) {
1721: return $newValueType;
1722: }
1723:
1724: return $traverse($type);
1725: });
1726: }
1727:
1728: if (!$changed) {
1729: return $this;
1730: }
1731:
1732: $result = $newTypes[0];
1733: for ($i = 1, $count = count($newTypes); $i < $count; $i++) {
1734: $result = TypeCombinator::intersect($result, $newTypes[$i]);
1735: }
1736: return $result;
1737: }
1738:
1739: return $this;
1740: }
1741:
1742: public function tryRemove(Type $typeToRemove): ?Type
1743: {
1744: return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1745: }
1746:
1747: public function exponentiate(Type $exponent): Type
1748: {
1749: return $this->intersectTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1750: }
1751:
1752: public function getFiniteTypes(): array
1753: {
1754: $compare = [];
1755: foreach ($this->types as $type) {
1756: $oneType = [];
1757: foreach ($type->getFiniteTypes() as $finiteType) {
1758: if ($finiteType instanceof EnumCaseObjectType) {
1759: $oneType[$finiteType->getClassName() . '::' . $finiteType->getEnumCaseName()] = $finiteType;
1760: continue;
1761: }
1762: $oneType[$finiteType->describe(VerbosityLevel::typeOnly())] = $finiteType;
1763: }
1764: $compare[] = $oneType;
1765: }
1766:
1767: $result = array_values(array_intersect_key(...$compare));
1768:
1769: if (count($result) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1770: return [];
1771: }
1772:
1773: return $result;
1774: }
1775:
1776: /**
1777: * @param callable(Type $type): TrinaryLogic $getResult
1778: * @param (callable(Type $type): bool)|null $filter
1779: */
1780: private function intersectResults(
1781: callable $getResult,
1782: ?callable $filter = null,
1783: ): TrinaryLogic
1784: {
1785: $types = $this->types;
1786: if ($filter !== null) {
1787: $types = array_filter($types, $filter);
1788: }
1789: if (count($types) === 0) {
1790: return TrinaryLogic::createNo();
1791: }
1792:
1793: return TrinaryLogic::lazyMaxMin($types, $getResult);
1794: }
1795:
1796: /**
1797: * @param callable(Type $type): Type $getType
1798: */
1799: private function intersectTypes(callable $getType): Type
1800: {
1801: $operands = array_map($getType, $this->types);
1802: $result = $operands[0];
1803: for ($i = 1, $count = count($operands); $i < $count; $i++) {
1804: $result = TypeCombinator::intersect($result, $operands[$i]);
1805: }
1806: return $result;
1807: }
1808:
1809: /**
1810: * @param callable(Type $type): Type $getType
1811: */
1812: private function intersectTypesPreserveTemplateType(callable $getType): Type
1813: {
1814: return $this->intersectTypes(static function (Type $type) use ($getType): Type {
1815: if ($type instanceof TemplateType) {
1816: return $type;
1817: }
1818: return $getType($type);
1819: });
1820: }
1821:
1822: public function toPhpDocNode(): TypeNode
1823: {
1824: $baseTypes = [];
1825: $typesToDescribe = [];
1826: $skipTypeNames = [];
1827:
1828: $nonEmptyStr = false;
1829: $nonFalsyStr = false;
1830: $isList = $this->isList()->yes();
1831: $isArray = $this->isArray()->yes();
1832: $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
1833: $describedTypes = [];
1834:
1835: foreach ($this->getSortedTypes() as $i => $type) {
1836: if ($type instanceof AccessoryNonEmptyStringType
1837: || $type instanceof AccessoryLiteralStringType
1838: || $type instanceof AccessoryNumericStringType
1839: || $type instanceof AccessoryNonFalsyStringType
1840: || $type instanceof AccessoryLowercaseStringType
1841: || $type instanceof AccessoryUppercaseStringType
1842: || $type instanceof AccessoryDecimalIntegerStringType
1843: ) {
1844: if ($type instanceof AccessoryNonFalsyStringType) {
1845: $nonFalsyStr = true;
1846: }
1847: if ($type instanceof AccessoryNonEmptyStringType) {
1848: $nonEmptyStr = true;
1849: }
1850: if ($nonEmptyStr && $nonFalsyStr) {
1851: // prevent redundant 'non-empty-string&non-falsy-string'
1852: foreach ($typesToDescribe as $key => $typeToDescribe) {
1853: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
1854: continue;
1855: }
1856:
1857: unset($typesToDescribe[$key]);
1858: }
1859: }
1860:
1861: $typesToDescribe[$i] = $type;
1862: $skipTypeNames[] = 'string';
1863: continue;
1864: }
1865:
1866: if ($isList || $isArray) {
1867: if ($type instanceof ArrayType) {
1868: $keyType = $type->getKeyType();
1869: $valueType = $type->getItemType();
1870: if ($isList) {
1871: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1872: $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-list' : 'list');
1873: if (!$isMixedValueType) {
1874: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1875: $valueType->toPhpDocNode(),
1876: ]);
1877: } else {
1878: $describedTypes[$i] = $identifierTypeNode;
1879: }
1880: } else {
1881: $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
1882: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1883: $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-array' : 'array');
1884: if (!$isMixedKeyType) {
1885: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1886: $keyType->toPhpDocNode(),
1887: $valueType->toPhpDocNode(),
1888: ]);
1889: } elseif (!$isMixedValueType) {
1890: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1891: $valueType->toPhpDocNode(),
1892: ]);
1893: } else {
1894: $describedTypes[$i] = $identifierTypeNode;
1895: }
1896: }
1897: continue;
1898: } elseif ($type instanceof ConstantArrayType) {
1899: $constantArrayTypeNode = $type->toPhpDocNode();
1900: if ($constantArrayTypeNode instanceof ArrayShapeNode) {
1901: $newKind = $constantArrayTypeNode->kind;
1902: if ($isList) {
1903: if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1904: $newKind = ArrayShapeNode::KIND_NON_EMPTY_LIST;
1905: } else {
1906: $newKind = ArrayShapeNode::KIND_LIST;
1907: }
1908: } elseif ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1909: $newKind = ArrayShapeNode::KIND_NON_EMPTY_ARRAY;
1910: }
1911:
1912: if ($newKind !== $constantArrayTypeNode->kind) {
1913: if ($constantArrayTypeNode->sealed) {
1914: $constantArrayTypeNode = ArrayShapeNode::createSealed($constantArrayTypeNode->items, $newKind);
1915: } else {
1916: $constantArrayTypeNode = ArrayShapeNode::createUnsealed($constantArrayTypeNode->items, $constantArrayTypeNode->unsealedType, $newKind);
1917: }
1918: }
1919:
1920: $describedTypes[$i] = $constantArrayTypeNode;
1921: continue;
1922: }
1923: }
1924: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
1925: continue;
1926: }
1927: }
1928:
1929: if (!$type instanceof AccessoryType) {
1930: $baseTypes[$i] = $type;
1931: continue;
1932: }
1933:
1934: $accessoryPhpDocNode = $type->toPhpDocNode();
1935: if ($accessoryPhpDocNode instanceof IdentifierTypeNode && $accessoryPhpDocNode->name === '') {
1936: continue;
1937: }
1938:
1939: $typesToDescribe[$i] = $type;
1940: }
1941:
1942: foreach ($baseTypes as $i => $type) {
1943: $typeNode = $type->toPhpDocNode();
1944: if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') {
1945: $nonEmpty = false;
1946: $typeName = 'array';
1947: foreach ($typesToDescribe as $j => $typeToDescribe) {
1948: if ($typeToDescribe instanceof AccessoryArrayListType) {
1949: $typeName = 'list';
1950: if (count($typeNode->genericTypes) > 1) {
1951: array_shift($typeNode->genericTypes);
1952: }
1953: } elseif ($typeToDescribe instanceof NonEmptyArrayType) {
1954: $nonEmpty = true;
1955: } else {
1956: continue;
1957: }
1958:
1959: unset($typesToDescribe[$j]);
1960: }
1961:
1962: if ($nonEmpty) {
1963: $typeName = 'non-empty-' . $typeName;
1964: }
1965:
1966: $describedTypes[$i] = new GenericTypeNode(
1967: new IdentifierTypeNode($typeName),
1968: $typeNode->genericTypes,
1969: );
1970: continue;
1971: }
1972:
1973: if ($typeNode instanceof IdentifierTypeNode && in_array($typeNode->name, $skipTypeNames, true)) {
1974: continue;
1975: }
1976:
1977: $describedTypes[$i] = $typeNode;
1978: }
1979:
1980: foreach ($typesToDescribe as $i => $typeToDescribe) {
1981: $describedTypes[$i] = $typeToDescribe->toPhpDocNode();
1982: }
1983:
1984: ksort($describedTypes);
1985:
1986: $describedTypes = array_values($describedTypes);
1987:
1988: if (count($describedTypes) === 1) {
1989: return $describedTypes[0];
1990: }
1991:
1992: if (count($describedTypes) === 0) {
1993: throw new ShouldNotHappenException(sprintf('Intersection consists of %s but there should be at least one base type.', implode('&', array_map(static fn (Type $type) => $type->describe(VerbosityLevel::precise()), $this->types))));
1994: }
1995:
1996: return new IntersectionTypeNode($describedTypes);
1997: }
1998:
1999: public function hasTemplateOrLateResolvableType(): bool
2000: {
2001: foreach ($this->types as $type) {
2002: if (!$type->hasTemplateOrLateResolvableType()) {
2003: continue;
2004: }
2005:
2006: return true;
2007: }
2008:
2009: return false;
2010: }
2011:
2012: }
2013: