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