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