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