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