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