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