1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Internal\CombinationsHelper;
6: use PHPStan\Php\PhpVersion;
7: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
8: use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
9: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
10: use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
11: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
12: use PHPStan\Reflection\Callables\CallableParametersAcceptor;
13: use PHPStan\Reflection\ClassConstantReflection;
14: use PHPStan\Reflection\ClassMemberAccessAnswerer;
15: use PHPStan\Reflection\ExtendedMethodReflection;
16: use PHPStan\Reflection\ExtendedPropertyReflection;
17: use PHPStan\Reflection\InitializerExprTypeResolver;
18: use PHPStan\Reflection\MissingConstantFromReflectionException;
19: use PHPStan\Reflection\MissingMethodFromReflectionException;
20: use PHPStan\Reflection\MissingPropertyFromReflectionException;
21: use PHPStan\Reflection\ParametersAcceptorSelector;
22: use PHPStan\Reflection\ReflectionProvider;
23: use PHPStan\Reflection\TrivialParametersAcceptor;
24: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection;
25: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection;
26: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
27: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
28: use PHPStan\ShouldNotHappenException;
29: use PHPStan\TrinaryLogic;
30: use PHPStan\Type\Accessory\AccessoryArrayListType;
31: use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
32: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
33: use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
34: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
35: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
36: use PHPStan\Type\Accessory\AccessoryNumericStringType;
37: use PHPStan\Type\Accessory\AccessoryType;
38: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
39: use PHPStan\Type\Accessory\HasOffsetType;
40: use PHPStan\Type\Accessory\HasOffsetValueType;
41: use PHPStan\Type\Accessory\NonEmptyArrayType;
42: use PHPStan\Type\Constant\ConstantArrayType;
43: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
44: use PHPStan\Type\Constant\ConstantIntegerType;
45: use PHPStan\Type\Constant\ConstantStringType;
46: use PHPStan\Type\Enum\EnumCaseObjectType;
47: use PHPStan\Type\Generic\TemplateArrayType;
48: use PHPStan\Type\Generic\TemplateType;
49: use PHPStan\Type\Generic\TemplateTypeMap;
50: use PHPStan\Type\Generic\TemplateTypeVariance;
51: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
52: use PHPStan\Type\Traits\NonRemoveableTypeTrait;
53: use function array_filter;
54: use function array_intersect_key;
55: use function array_map;
56: use function array_shift;
57: use function array_unique;
58: use function array_values;
59: use function count;
60: use function implode;
61: use function in_array;
62: use function is_int;
63: use function ksort;
64: use function sprintf;
65: use function str_starts_with;
66: use function strcasecmp;
67: use function strlen;
68: use function substr;
69: use function usort;
70:
71: /** @api */
72: class IntersectionType implements CompoundType
73: {
74:
75: use NonRemoveableTypeTrait;
76: use NonGeneralizableTypeTrait;
77:
78: private bool $sortedTypes = false;
79:
80: private ?TrinaryLogic $isBoolean = null;
81:
82: private ?TrinaryLogic $isFloat = null;
83:
84: private ?TrinaryLogic $isInteger = null;
85:
86: private ?TrinaryLogic $isString = null;
87:
88: private ?TrinaryLogic $isArray = null;
89:
90: private ?TrinaryLogic $isList = null;
91:
92: private ?TrinaryLogic $isConstantArray = null;
93:
94: private ?TrinaryLogic $isOversizedArray = null;
95:
96: private ?TrinaryLogic $isOffsetAccessible = null;
97:
98: private ?TrinaryLogic $isIterableAtLeastOnce = null;
99:
100: private ?TrinaryLogic $isConstantScalarValue = null;
101:
102: private ?TrinaryLogic $isCallable = null;
103:
104: /** @var array<string, Type> */
105: private array $cachedGetOffsetValueType = [];
106:
107: /** @var array<string, TrinaryLogic> */
108: private array $cachedHasOffsetValueType = [];
109:
110: /** @var array<int, string> */
111: private array $cachedDescriptions = [];
112:
113: /**
114: * @api
115: * @param list<Type> $types
116: */
117: public function __construct(private array $types)
118: {
119: if (count($types) < 2) {
120: throw new ShouldNotHappenException(sprintf(
121: 'Cannot create %s with: %s',
122: self::class,
123: implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)),
124: ));
125: }
126: }
127:
128: /**
129: * @return list<Type>
130: */
131: public function getTypes(): array
132: {
133: return $this->types;
134: }
135:
136: /**
137: * @return list<Type>
138: */
139: private function getSortedTypes(): array
140: {
141: if ($this->sortedTypes) {
142: return $this->types;
143: }
144:
145: $this->types = UnionTypeHelper::sortTypes($this->types);
146: $this->sortedTypes = true;
147:
148: return $this->types;
149: }
150:
151: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
152: {
153: $types = TemplateTypeMap::createEmpty();
154:
155: foreach ($this->types as $type) {
156: $types = $types->intersect($templateType->inferTemplateTypes($type));
157: }
158:
159: return $types;
160: }
161:
162: public function getReferencedClasses(): array
163: {
164: $classes = [];
165: foreach ($this->types as $type) {
166: foreach ($type->getReferencedClasses() as $className) {
167: $classes[] = $className;
168: }
169: }
170:
171: return $classes;
172: }
173:
174: public function getObjectClassNames(): array
175: {
176: $objectClassNames = [];
177: foreach ($this->types as $type) {
178: $innerObjectClassNames = $type->getObjectClassNames();
179: foreach ($innerObjectClassNames as $innerObjectClassName) {
180: $objectClassNames[] = $innerObjectClassName;
181: }
182: }
183:
184: return array_values(array_unique($objectClassNames));
185: }
186:
187: public function getObjectClassReflections(): array
188: {
189: $reflections = [];
190: foreach ($this->types as $type) {
191: foreach ($type->getObjectClassReflections() as $reflection) {
192: $reflections[] = $reflection;
193: }
194: }
195:
196: return $reflections;
197: }
198:
199: public function getArrays(): array
200: {
201: $arrays = [];
202: foreach ($this->types as $type) {
203: foreach ($type->getArrays() as $array) {
204: $arrays[] = $array;
205: }
206: }
207:
208: return $arrays;
209: }
210:
211: public function getConstantArrays(): array
212: {
213: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
214: $builder = ConstantArrayTypeBuilder::createEmpty();
215: $zero = new ConstantIntegerType(0);
216: $builder->setOffsetValueType(
217: $zero,
218: $this->getOffsetValueType($zero),
219: );
220: $one = new ConstantIntegerType(1);
221: $builder->setOffsetValueType(
222: $one,
223: $this->getOffsetValueType($one),
224: );
225: $constantArray = $builder->getArray();
226: if (!$constantArray instanceof ConstantArrayType) {
227: throw new ShouldNotHappenException();
228: }
229:
230: return [$builder->getArray()];
231: }
232:
233: $constantArrays = [];
234: foreach ($this->types as $type) {
235: foreach ($type->getConstantArrays() as $constantArray) {
236: $constantArrays[] = $constantArray;
237: }
238: }
239:
240: return $constantArrays;
241: }
242:
243: public function getConstantStrings(): array
244: {
245: $strings = [];
246: foreach ($this->types as $type) {
247: foreach ($type->getConstantStrings() as $string) {
248: $strings[] = $string;
249: }
250: }
251:
252: return $strings;
253: }
254:
255: public function accepts(Type $otherType, bool $strictTypes): AcceptsResult
256: {
257: $result = AcceptsResult::createYes();
258: foreach ($this->types as $type) {
259: $result = $result->and($type->accepts($otherType, $strictTypes));
260: }
261:
262: if (!$result->yes()) {
263: $isList = $otherType->isList();
264: $reasons = $result->reasons;
265: $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $otherType);
266: if ($this->isList()->yes() && !$isList->yes()) {
267: $reasons[] = sprintf(
268: '%s %s a list.',
269: $otherType->describe($verbosity),
270: $isList->no() ? 'is not' : 'might not be',
271: );
272: }
273:
274: $isNonEmpty = $otherType->isIterableAtLeastOnce();
275: if ($this->isIterableAtLeastOnce()->yes() && !$isNonEmpty->yes()) {
276: $reasons[] = sprintf(
277: '%s %s empty.',
278: $otherType->describe($verbosity),
279: $isNonEmpty->no() ? 'is' : 'might be',
280: );
281: }
282:
283: if (count($reasons) > 0) {
284: return new AcceptsResult($result->result, $reasons);
285: }
286: }
287:
288: return $result;
289: }
290:
291: public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult
292: {
293: if ($otherType instanceof IntersectionType && $this->equals($otherType)) {
294: return IsSuperTypeOfResult::createYes();
295: }
296:
297: if ($otherType instanceof NeverType) {
298: return IsSuperTypeOfResult::createYes();
299: }
300:
301: return IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types));
302: }
303:
304: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
305: {
306: if (($otherType instanceof self || $otherType instanceof UnionType) && !$otherType instanceof TemplateType) {
307: return $otherType->isSuperTypeOf($this);
308: }
309:
310: $result = IsSuperTypeOfResult::lazyMaxMin(
311: $this->types,
312: static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType),
313: );
314:
315: if (
316: !$result->no()
317: && $this->isOversizedArray()->yes()
318: && !$otherType->isIterableAtLeastOnce()->no()
319: ) {
320: return IsSuperTypeOfResult::createYes();
321: }
322:
323: return $result;
324: }
325:
326: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
327: {
328: $result = AcceptsResult::lazyMaxMin(
329: $this->types,
330: static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes),
331: );
332:
333: // lazyMaxMin can short-circuit to Yes when array<mixed> (inside e.g. array&callable
334: // or array&hasOffsetValue) is accepted by a specific array type like array<int>,
335: // because MixedType::isAcceptedBy() always returns Yes. The isSuperTypeOf check
336: // considers the intersection holistically and catches these false positives.
337: if ($result->yes()) {
338: $isSuperType = $acceptingType->isSuperTypeOf($this);
339: if ($isSuperType->no()) {
340: return $isSuperType->toAcceptsResult();
341: }
342:
343: // A TemplateType member accepts eagerly, so lazyMaxMin's Yes may come solely from it.
344: // A Maybe from the holistic isSuperTypeOf means no member is a definite subtype, so
345: // when a TemplateType member forced the eager Yes, that Yes is untrustworthy - trust the Maybe.
346: // Only distrust when a TemplateType member itself accepts with Yes; if it accepts with Maybe,
347: // the eager Yes came from a non-template member and reflects a genuine (object-level) match.
348: if ($isSuperType->maybe()) {
349: foreach ($this->types as $innerType) {
350: if ($innerType instanceof TemplateType && $acceptingType->accepts($innerType, $strictTypes)->yes()) {
351: return $isSuperType->toAcceptsResult();
352: }
353: }
354: }
355: }
356:
357: if ($this->isOversizedArray()->yes()) {
358: if (!$result->no()) {
359: return AcceptsResult::createYes();
360: }
361: }
362:
363: return $result;
364: }
365:
366: public function equals(Type $type): bool
367: {
368: if (!$type instanceof static) {
369: return false;
370: }
371:
372: if (count($this->types) !== count($type->types)) {
373: return false;
374: }
375:
376: $otherTypes = $type->types;
377: foreach ($this->types as $innerType) {
378: $match = false;
379: foreach ($otherTypes as $i => $otherType) {
380: if (!$innerType->equals($otherType)) {
381: continue;
382: }
383:
384: $match = true;
385: unset($otherTypes[$i]);
386: break;
387: }
388:
389: if (!$match) {
390: return false;
391: }
392: }
393:
394: return count($otherTypes) === 0;
395: }
396:
397: public function describe(VerbosityLevel $level): string
398: {
399: if (isset($this->cachedDescriptions[$level->getLevelValue()])) {
400: return $this->cachedDescriptions[$level->getLevelValue()];
401: }
402:
403: return $this->cachedDescriptions[$level->getLevelValue()] = $level->handle(
404: fn (): string => $this->describeType($level),
405: fn (): string => $this->describeItself($level, true),
406: fn (): string => $this->describeItself($level, false),
407: );
408: }
409:
410: private function describeType(VerbosityLevel $level): string
411: {
412: $typeNames = [];
413: $isList = $this->isList()->yes();
414: $valueType = null;
415: foreach ($this->getSortedTypes() as $type) {
416: if ($isList) {
417: if ($type instanceof ArrayType || $type instanceof ConstantArrayType) {
418: $valueType = $type->getIterableValueType();
419: continue;
420: }
421: if ($type instanceof NonEmptyArrayType) {
422: continue;
423: }
424: }
425: if ($type instanceof AccessoryType) {
426: continue;
427: }
428: $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level);
429: }
430:
431: if ($isList) {
432: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
433: $innerType = '';
434: if ($valueType !== null && !$isMixedValueType) {
435: $innerType = sprintf('<%s>', $valueType->describe($level));
436: }
437:
438: $typeNames[] = 'list' . $innerType;
439: }
440:
441: usort($typeNames, static function ($a, $b) {
442: $cmp = strcasecmp($a, $b);
443: if ($cmp !== 0) {
444: return $cmp;
445: }
446:
447: return $a <=> $b;
448: });
449:
450: return implode('&', $typeNames);
451: }
452:
453: private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string
454: {
455: $baseTypes = [];
456: $typesToDescribe = [];
457: $skipTypeNames = [];
458:
459: $nonEmptyStr = false;
460: $nonFalsyStr = false;
461: $isList = $this->isList()->yes();
462: $isArray = $this->isArray()->yes();
463: $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
464: // When a TemplateArrayType carries the array refinement, we describe
465: // it via its own describe() (e.g. "T of array") rather than collapsing
466: // it into a generic `array<...>` prefix. In that case the
467: // `NonEmptyArrayType` and `AccessoryArrayListType` markers must
468: // describe themselves explicitly — they cannot be absorbed into a
469: // non-existent `non-empty-array` prefix.
470: $hasTemplateArray = false;
471: if ($isArray || $isList) {
472: foreach ($this->types as $type) {
473: if ($type instanceof TemplateArrayType) {
474: $hasTemplateArray = true;
475: break;
476: }
477: }
478: }
479: $describedTypes = [];
480: foreach ($this->getSortedTypes() as $i => $type) {
481: if ($type instanceof AccessoryNonEmptyStringType
482: || $type instanceof AccessoryLiteralStringType
483: || $type instanceof AccessoryNumericStringType
484: || $type instanceof AccessoryNonFalsyStringType
485: || $type instanceof AccessoryLowercaseStringType
486: || $type instanceof AccessoryUppercaseStringType
487: || $type instanceof AccessoryDecimalIntegerStringType
488: ) {
489: if (
490: ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType)
491: && !$level->isPrecise()
492: && !$level->isCache()
493: ) {
494: continue;
495: }
496: if ($type instanceof AccessoryNonFalsyStringType) {
497: $nonFalsyStr = true;
498: }
499: if ($type instanceof AccessoryNonEmptyStringType) {
500: $nonEmptyStr = true;
501: }
502: if ($nonEmptyStr && $nonFalsyStr) {
503: // prevent redundant 'non-empty-string&non-falsy-string'
504: foreach ($typesToDescribe as $key => $typeToDescribe) {
505: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
506: continue;
507: }
508:
509: unset($typesToDescribe[$key]);
510: }
511: }
512:
513: $typesToDescribe[$i] = $type;
514: $skipTypeNames[] = 'string';
515: continue;
516: }
517: if ($isList || $isArray) {
518: if ($type instanceof TemplateArrayType) {
519: // Preserve the template's own describe (e.g. "T of array")
520: // instead of collapsing it to a generic array shape — the
521: // other intersection members already carry the array
522: // refinement.
523: $describedTypes[$i] = $type->describe($level);
524: continue;
525: }
526: if ($type instanceof ArrayType) {
527: $keyType = $type->getKeyType();
528: $valueType = $type->getItemType();
529: if ($isList) {
530: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
531: $valueTypeDescription = '';
532: if (!$isMixedValueType) {
533: $valueTypeDescription = sprintf('<%s>', $valueType->describe($level));
534: }
535:
536: $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-list' : 'list') . $valueTypeDescription;
537: } else {
538: $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
539: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
540: $typeDescription = '';
541: if (!$isMixedKeyType) {
542: $typeDescription = sprintf('<%s, %s>', $keyType->describe($level), $valueType->describe($level));
543: } elseif (!$isMixedValueType) {
544: $typeDescription = sprintf('<%s>', $valueType->describe($level));
545: }
546:
547: $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-array' : 'array') . $typeDescription;
548: }
549: continue;
550: } elseif ($type instanceof ConstantArrayType) {
551: $description = $type->describe($level);
552: $kind = str_starts_with($description, 'list') ? 'list' : 'array';
553: $descriptionWithoutKind = substr($description, strlen($kind));
554: $begin = $isList ? 'list' : 'array';
555: if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
556: $begin = 'non-empty-' . $begin;
557: }
558:
559: $describedTypes[$i] = $begin . $descriptionWithoutKind;
560: continue;
561: }
562: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
563: if ($hasTemplateArray) {
564: $describedTypes[$i] = $type->describe($level);
565: }
566: continue;
567: }
568: }
569:
570: if ($type instanceof CallableType && $type->isCommonCallable()) {
571: $typesToDescribe[$i] = $type;
572: $skipTypeNames[] = 'object';
573: $skipTypeNames[] = 'string';
574: continue;
575: }
576:
577: if (!$type instanceof AccessoryType) {
578: $baseTypes[$i] = $type;
579: continue;
580: }
581:
582: if ($skipAccessoryTypes) {
583: continue;
584: }
585:
586: $typesToDescribe[$i] = $type;
587: }
588:
589: foreach ($baseTypes as $i => $type) {
590: $typeDescription = $type->describe($level);
591:
592: if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) {
593: foreach ($typesToDescribe as $j => $typeToDescribe) {
594: if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) {
595: $describedTypes[$i] = 'callable-' . $typeDescription;
596: unset($typesToDescribe[$j]);
597: continue 2;
598: }
599: }
600: }
601:
602: if (in_array($typeDescription, $skipTypeNames, true)) {
603: continue;
604: }
605:
606: $describedTypes[$i] = $type->describe($level);
607: }
608:
609: foreach ($typesToDescribe as $i => $typeToDescribe) {
610: $describedTypes[$i] = $typeToDescribe->describe($level);
611: }
612:
613: ksort($describedTypes);
614:
615: return implode('&', $describedTypes);
616: }
617:
618: public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type
619: {
620: return $this->intersectTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName));
621: }
622:
623: public function isObject(): TrinaryLogic
624: {
625: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isObject());
626: }
627:
628: public function getClassStringType(): Type
629: {
630: return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringType());
631: }
632:
633: public function isEnum(): TrinaryLogic
634: {
635: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isEnum());
636: }
637:
638: public function canAccessProperties(): TrinaryLogic
639: {
640: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
641: }
642:
643: public function hasProperty(string $propertyName): TrinaryLogic
644: {
645: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
646: }
647:
648: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
649: {
650: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
651: }
652:
653: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
654: {
655: $propertyPrototypes = [];
656: foreach ($this->types as $type) {
657: if (!$type->hasProperty($propertyName)->yes()) {
658: continue;
659: }
660:
661: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
662: }
663:
664: $propertiesCount = count($propertyPrototypes);
665: if ($propertiesCount === 0) {
666: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
667: }
668:
669: if ($propertiesCount === 1) {
670: return $propertyPrototypes[0];
671: }
672:
673: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
674: }
675:
676: public function hasInstanceProperty(string $propertyName): TrinaryLogic
677: {
678: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName));
679: }
680:
681: public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
682: {
683: return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty();
684: }
685:
686: public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
687: {
688: $propertyPrototypes = [];
689: foreach ($this->types as $type) {
690: if (!$type->hasInstanceProperty($propertyName)->yes()) {
691: continue;
692: }
693:
694: $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this);
695: }
696:
697: $propertiesCount = count($propertyPrototypes);
698: if ($propertiesCount === 0) {
699: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
700: }
701:
702: if ($propertiesCount === 1) {
703: return $propertyPrototypes[0];
704: }
705:
706: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
707: }
708:
709: public function hasStaticProperty(string $propertyName): TrinaryLogic
710: {
711: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName));
712: }
713:
714: public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
715: {
716: return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty();
717: }
718:
719: public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
720: {
721: $propertyPrototypes = [];
722: foreach ($this->types as $type) {
723: if (!$type->hasStaticProperty($propertyName)->yes()) {
724: continue;
725: }
726:
727: $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
728: }
729:
730: $propertiesCount = count($propertyPrototypes);
731: if ($propertiesCount === 0) {
732: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
733: }
734:
735: if ($propertiesCount === 1) {
736: return $propertyPrototypes[0];
737: }
738:
739: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
740: }
741:
742: public function canCallMethods(): TrinaryLogic
743: {
744: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
745: }
746:
747: public function hasMethod(string $methodName): TrinaryLogic
748: {
749: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
750: }
751:
752: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
753: {
754: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
755: }
756:
757: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
758: {
759: $methodPrototypes = [];
760: foreach ($this->types as $type) {
761: if (!$type->hasMethod($methodName)->yes()) {
762: continue;
763: }
764:
765: $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this);
766: }
767:
768: $methodsCount = count($methodPrototypes);
769: if ($methodsCount === 0) {
770: throw new MissingMethodFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $methodName);
771: }
772:
773: if ($methodsCount === 1) {
774: return $methodPrototypes[0];
775: }
776:
777: return new IntersectionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
778: }
779:
780: public function canAccessConstants(): TrinaryLogic
781: {
782: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
783: }
784:
785: public function hasConstant(string $constantName): TrinaryLogic
786: {
787: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName));
788: }
789:
790: public function getConstant(string $constantName): ClassConstantReflection
791: {
792: foreach ($this->types as $type) {
793: if ($type->hasConstant($constantName)->yes()) {
794: return $type->getConstant($constantName);
795: }
796: }
797:
798: throw new MissingConstantFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $constantName);
799: }
800:
801: public function isIterable(): TrinaryLogic
802: {
803: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
804: }
805:
806: public function isIterableAtLeastOnce(): TrinaryLogic
807: {
808: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
809: return TrinaryLogic::createYes();
810: }
811:
812: return $this->isIterableAtLeastOnce ??= $this->intersectResults(
813: static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce(),
814: static fn (Type $type): bool => !$type->isIterable()->no(),
815: );
816: }
817:
818: public function getArraySize(): Type
819: {
820: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
821: return new ConstantIntegerType(2);
822: }
823:
824: $arraySize = $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize());
825:
826: $knownOffsets = [];
827: foreach ($this->types as $type) {
828: if (!($type instanceof HasOffsetValueType) && !($type instanceof HasOffsetType)) {
829: continue;
830: }
831:
832: $knownOffsets[$type->getOffsetType()->getValue()] = true;
833: }
834:
835: if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) {
836: $knownOffsets[0] = true;
837: }
838:
839: if ($knownOffsets !== []) {
840: return TypeCombinator::intersect($arraySize, IntegerRangeType::fromInterval(count($knownOffsets), null));
841: }
842:
843: return $arraySize;
844: }
845:
846: public function getIterableKeyType(): Type
847: {
848: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
849: return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
850: }
851: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
852: }
853:
854: public function getFirstIterableKeyType(): Type
855: {
856: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
857: }
858:
859: public function getLastIterableKeyType(): Type
860: {
861: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
862: }
863:
864: public function getIterableValueType(): Type
865: {
866: $result = $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
867: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
868: return TypeCombinator::intersect(
869: $result,
870: new UnionType([
871: new ObjectWithoutClassType(),
872: new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]),
873: ]),
874: );
875: }
876: return $result;
877: }
878:
879: public function getFirstIterableValueType(): Type
880: {
881: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
882: }
883:
884: public function getLastIterableValueType(): Type
885: {
886: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
887: }
888:
889: public function isArray(): TrinaryLogic
890: {
891: return $this->isArray ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray());
892: }
893:
894: public function isConstantArray(): TrinaryLogic
895: {
896: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
897: return TrinaryLogic::createYes();
898: }
899: return $this->isConstantArray ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray());
900: }
901:
902: public function isOversizedArray(): TrinaryLogic
903: {
904: return $this->isOversizedArray ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
905: }
906:
907: public function isList(): TrinaryLogic
908: {
909: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
910: return TrinaryLogic::createYes();
911: }
912:
913: return $this->isList ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isList());
914: }
915:
916: public function isString(): TrinaryLogic
917: {
918: return $this->isString ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString());
919: }
920:
921: public function isNumericString(): TrinaryLogic
922: {
923: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
924: }
925:
926: public function isDecimalIntegerString(): TrinaryLogic
927: {
928: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isDecimalIntegerString());
929: }
930:
931: public function isNonEmptyString(): TrinaryLogic
932: {
933: if ($this->isCallable()->yes() && $this->isString()->yes()) {
934: return TrinaryLogic::createYes();
935: }
936: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
937: }
938:
939: public function isNonFalsyString(): TrinaryLogic
940: {
941: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
942: }
943:
944: public function isLiteralString(): TrinaryLogic
945: {
946: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
947: }
948:
949: public function isLowercaseString(): TrinaryLogic
950: {
951: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString());
952: }
953:
954: public function isUppercaseString(): TrinaryLogic
955: {
956: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString());
957: }
958:
959: public function isClassString(): TrinaryLogic
960: {
961: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassString());
962: }
963:
964: public function getClassStringObjectType(): Type
965: {
966: return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringObjectType());
967: }
968:
969: public function getObjectTypeOrClassStringObjectType(): Type
970: {
971: return $this->intersectTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType());
972: }
973:
974: public function isVoid(): TrinaryLogic
975: {
976: return TrinaryLogic::createNo();
977: }
978:
979: public function isScalar(): TrinaryLogic
980: {
981: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isScalar());
982: }
983:
984: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
985: {
986: return $this->intersectResults(
987: static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic(),
988: )->toBooleanType();
989: }
990:
991: public function isOffsetAccessible(): TrinaryLogic
992: {
993: return $this->isOffsetAccessible ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
994: }
995:
996: public function isOffsetAccessLegal(): TrinaryLogic
997: {
998: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
999: }
1000:
1001: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
1002: {
1003: $cacheKey = $offsetType->describe(VerbosityLevel::cache());
1004: if (isset($this->cachedHasOffsetValueType[$cacheKey])) {
1005: return $this->cachedHasOffsetValueType[$cacheKey];
1006: }
1007: return $this->cachedHasOffsetValueType[$cacheKey] = $this->doHasOffsetValueType($offsetType);
1008: }
1009:
1010: private function doHasOffsetValueType(Type $offsetType): TrinaryLogic
1011: {
1012: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
1013: $arrayKeyOffsetType = $offsetType->toArrayKey();
1014: $callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
1015:
1016: return $callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->result;
1017: }
1018:
1019: if ($this->isList()->yes()) {
1020: $arrayKeyOffsetType = $offsetType->toArrayKey();
1021:
1022: $negative = IntegerRangeType::fromInterval(null, -1);
1023: if ($negative->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1024: return TrinaryLogic::createNo();
1025: }
1026:
1027: $size = $this->getArraySize();
1028: if ($size instanceof IntegerRangeType && $size->getMin() !== null) {
1029: $knownOffsets = IntegerRangeType::fromInterval(0, $size->getMin() - 1);
1030: } elseif ($size instanceof ConstantIntegerType) {
1031: $knownOffsets = IntegerRangeType::fromInterval(0, $size->getValue() - 1);
1032: } elseif ($this->isIterableAtLeastOnce()->yes()) {
1033: $knownOffsets = new ConstantIntegerType(0);
1034: } else {
1035: $knownOffsets = null;
1036: }
1037:
1038: if ($knownOffsets !== null && $knownOffsets->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1039: return TrinaryLogic::createYes();
1040: }
1041:
1042: foreach ($this->types as $type) {
1043: if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
1044: continue;
1045: }
1046:
1047: foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
1048: if (!is_int($constantScalarValue)) {
1049: continue;
1050: }
1051: if (IntegerRangeType::fromInterval(0, $constantScalarValue)->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1052: return TrinaryLogic::createYes();
1053: }
1054: }
1055: }
1056: }
1057:
1058: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
1059: }
1060:
1061: public function getOffsetValueType(Type $offsetType): Type
1062: {
1063: $cacheKey = $offsetType->describe(VerbosityLevel::cache());
1064: if (isset($this->cachedGetOffsetValueType[$cacheKey])) {
1065: return $this->cachedGetOffsetValueType[$cacheKey];
1066: }
1067: return $this->cachedGetOffsetValueType[$cacheKey] = $this->doGetOffsetValueType($offsetType);
1068: }
1069:
1070: private function doGetOffsetValueType(Type $offsetType): Type
1071: {
1072: $result = $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType));
1073: if ($this->isOversizedArray()->yes()) {
1074: return TypeUtils::toBenevolentUnion($result);
1075: }
1076:
1077: if ($this->isCallable()->yes() && $this->isArray()->yes()) {
1078: $arrayKeyOffsetType = $offsetType->toArrayKey();
1079: if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1080: $narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]);
1081: } elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
1082: $narrowedType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]);
1083: } else {
1084: $narrowedType = new UnionType([new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), new ObjectWithoutClassType()]);
1085: }
1086: $result = TypeCombinator::intersect($result, $narrowedType);
1087: }
1088:
1089: return $result;
1090: }
1091:
1092: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
1093: {
1094: if ($this->isOversizedArray()->yes()) {
1095: return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type {
1096: // avoid new HasOffsetValueType being intersected with oversized array
1097: if (!$type instanceof ArrayType) {
1098: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
1099: }
1100:
1101: if (!$offsetType instanceof ConstantStringType && !$offsetType instanceof ConstantIntegerType) {
1102: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
1103: }
1104:
1105: if (!$offsetType->isSuperTypeOf($type->getKeyType())->yes()) {
1106: return $type->setOffsetValueType($offsetType, $valueType, $unionValues);
1107: }
1108:
1109: return new IntersectionType([
1110: new ArrayType(
1111: TypeCombinator::union($type->getKeyType(), $offsetType),
1112: TypeCombinator::union($type->getItemType(), $valueType),
1113: ),
1114: new NonEmptyArrayType(),
1115: ]);
1116: });
1117: }
1118:
1119: $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
1120:
1121: if (
1122: $offsetType !== null
1123: && $this->isList()->yes()
1124: && !$result->isList()->yes()
1125: ) {
1126: if ($this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) {
1127: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
1128: } else {
1129: foreach ($this->types as $type) {
1130: if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
1131: continue;
1132: }
1133:
1134: foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
1135: if (!is_int($constantScalarValue)) {
1136: continue;
1137: }
1138: if (IntegerRangeType::fromInterval(0, $constantScalarValue + 1)->isSuperTypeOf($offsetType)->yes()) {
1139: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
1140: break 2;
1141: }
1142: }
1143: }
1144: }
1145: }
1146:
1147: if (
1148: $this->isList()->yes()
1149: && $offsetType !== null
1150: && $offsetType->toArrayKey()->isInteger()->yes()
1151: && $this->getIterableValueType()->isArray()->yes()
1152: ) {
1153: $result = TypeCombinator::intersect($result, new AccessoryArrayListType());
1154: }
1155:
1156: return $result;
1157: }
1158:
1159: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
1160: {
1161: return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
1162: }
1163:
1164: public function unsetOffset(Type $offsetType): Type
1165: {
1166: return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
1167: }
1168:
1169: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
1170: {
1171: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict));
1172: }
1173:
1174: public function getKeysArray(): Type
1175: {
1176: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray());
1177: }
1178:
1179: public function getValuesArray(): Type
1180: {
1181: $cb = static fn (Type $type): Type => $type->getValuesArray();
1182: if ($this->isList()->yes()) {
1183: return $this;
1184: }
1185: return $this->intersectTypes($cb);
1186: }
1187:
1188: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
1189: {
1190: return $this->intersectTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
1191: }
1192:
1193: public function fillKeysArray(Type $valueType): Type
1194: {
1195: return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
1196: }
1197:
1198: public function flipArray(): Type
1199: {
1200: return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray());
1201: }
1202:
1203: public function intersectKeyArray(Type $otherArraysType): Type
1204: {
1205: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
1206: }
1207:
1208: public function popArray(): Type
1209: {
1210: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->popArray());
1211: }
1212:
1213: public function reverseArray(TrinaryLogic $preserveKeys): Type
1214: {
1215: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->reverseArray($preserveKeys));
1216: }
1217:
1218: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
1219: {
1220: return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict));
1221: }
1222:
1223: public function shiftArray(): Type
1224: {
1225: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->shiftArray());
1226: }
1227:
1228: public function shuffleArray(): Type
1229: {
1230: $cb = static fn (Type $type): Type => $type->shuffleArray();
1231: if ($this->isList()->yes()) {
1232: return $this->intersectTypesPreserveTemplateType($cb);
1233: }
1234: return $this->intersectTypes($cb);
1235: }
1236:
1237: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
1238: {
1239: $result = $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
1240:
1241: if (
1242: $this->isList()->yes()
1243: && $this->isIterableAtLeastOnce()->yes()
1244: && (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()
1245: && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()
1246: ) {
1247: $result = TypeCombinator::intersect($result, new NonEmptyArrayType());
1248: }
1249:
1250: return $result;
1251: }
1252:
1253: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1254: {
1255: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
1256: }
1257:
1258: public function truncateListToSize(Type $sizeType): Type
1259: {
1260: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->truncateListToSize($sizeType));
1261: }
1262:
1263: public function makeListMaybe(): Type
1264: {
1265: return $this->intersectTypes(static fn (Type $type): Type => $type->makeListMaybe());
1266: }
1267:
1268: public function mapValueType(callable $cb): Type
1269: {
1270: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->mapValueType($cb));
1271: }
1272:
1273: public function mapKeyType(callable $cb): Type
1274: {
1275: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->mapKeyType($cb));
1276: }
1277:
1278: public function makeAllArrayKeysOptional(): Type
1279: {
1280: return $this->intersectTypes(static fn (Type $type): Type => $type->makeAllArrayKeysOptional());
1281: }
1282:
1283: public function changeKeyCaseArray(?int $case): Type
1284: {
1285: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->changeKeyCaseArray($case));
1286: }
1287:
1288: public function filterArrayRemovingFalsey(): Type
1289: {
1290: return $this->intersectTypesPreserveTemplateType(static fn (Type $type): Type => $type->filterArrayRemovingFalsey());
1291: }
1292:
1293: public function getEnumCases(): array
1294: {
1295: $compare = [];
1296: foreach ($this->types as $type) {
1297: $oneType = [];
1298: foreach ($type->getEnumCases() as $enumCase) {
1299: $oneType[$enumCase->getClassName() . '::' . $enumCase->getEnumCaseName()] = $enumCase;
1300: }
1301: $compare[] = $oneType;
1302: }
1303:
1304: return array_values(array_intersect_key(...$compare));
1305: }
1306:
1307: public function getEnumCaseObject(): ?EnumCaseObjectType
1308: {
1309: $singleCase = null;
1310: foreach ($this->types as $type) {
1311: $caseObject = $type->getEnumCaseObject();
1312: if ($caseObject === null) {
1313: continue;
1314: }
1315:
1316: if ($singleCase !== null) {
1317: return null;
1318: }
1319:
1320: $singleCase = $caseObject;
1321: }
1322:
1323: return $singleCase;
1324: }
1325:
1326: public function isCallable(): TrinaryLogic
1327: {
1328: return $this->isCallable ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
1329: }
1330:
1331: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
1332: {
1333: $yesAcceptors = [];
1334:
1335: foreach ($this->types as $type) {
1336: if (!$type->isCallable()->yes()) {
1337: continue;
1338: }
1339: $yesAcceptors[] = $type->getCallableParametersAcceptors($scope);
1340: }
1341:
1342: if (count($yesAcceptors) === 0) {
1343: if ($this->isCallable()->no()) {
1344: throw new ShouldNotHappenException();
1345: }
1346:
1347: return [new TrivialParametersAcceptor()];
1348: }
1349:
1350: $result = [];
1351: $combinations = CombinationsHelper::combinations($yesAcceptors);
1352: foreach ($combinations as $combination) {
1353: $combined = ParametersAcceptorSelector::combineAcceptors($combination);
1354: if (!$combined instanceof CallableParametersAcceptor) {
1355: throw new ShouldNotHappenException();
1356: }
1357: $result[] = $combined;
1358: }
1359:
1360: return $result;
1361: }
1362:
1363: public function isCloneable(): TrinaryLogic
1364: {
1365: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
1366: }
1367:
1368: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1369: {
1370: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion));
1371: }
1372:
1373: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1374: {
1375: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion));
1376: }
1377:
1378: public function isNull(): TrinaryLogic
1379: {
1380: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNull());
1381: }
1382:
1383: public function isConstantValue(): TrinaryLogic
1384: {
1385: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
1386: }
1387:
1388: public function isConstantScalarValue(): TrinaryLogic
1389: {
1390: return $this->isConstantScalarValue ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
1391: }
1392:
1393: public function getConstantScalarTypes(): array
1394: {
1395: $scalarTypes = [];
1396: foreach ($this->types as $type) {
1397: foreach ($type->getConstantScalarTypes() as $scalarType) {
1398: $scalarTypes[] = $scalarType;
1399: }
1400: }
1401:
1402: return $scalarTypes;
1403: }
1404:
1405: public function getConstantScalarValues(): array
1406: {
1407: $values = [];
1408: foreach ($this->types as $type) {
1409: foreach ($type->getConstantScalarValues() as $value) {
1410: $values[] = $value;
1411: }
1412: }
1413:
1414: return $values;
1415: }
1416:
1417: public function isTrue(): TrinaryLogic
1418: {
1419: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
1420: }
1421:
1422: public function isFalse(): TrinaryLogic
1423: {
1424: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
1425: }
1426:
1427: public function isBoolean(): TrinaryLogic
1428: {
1429: return $this->isBoolean ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
1430: }
1431:
1432: public function isFloat(): TrinaryLogic
1433: {
1434: return $this->isFloat ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
1435: }
1436:
1437: public function isInteger(): TrinaryLogic
1438: {
1439: return $this->isInteger ??= $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
1440: }
1441:
1442: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1443: {
1444: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion));
1445: }
1446:
1447: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1448: {
1449: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion));
1450: }
1451:
1452: public function getSmallerType(PhpVersion $phpVersion): Type
1453: {
1454: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion));
1455: }
1456:
1457: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
1458: {
1459: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion));
1460: }
1461:
1462: public function getGreaterType(PhpVersion $phpVersion): Type
1463: {
1464: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion));
1465: }
1466:
1467: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
1468: {
1469: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion));
1470: }
1471:
1472: public function toBoolean(): BooleanType
1473: {
1474: $type = $this->intersectTypes(static fn (Type $type): BooleanType => $type->toBoolean());
1475:
1476: if (!$type instanceof BooleanType) {
1477: return new BooleanType();
1478: }
1479:
1480: return $type;
1481: }
1482:
1483: public function toNumber(): Type
1484: {
1485: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toNumber());
1486:
1487: return $type;
1488: }
1489:
1490: public function toBitwiseNotType(): Type
1491: {
1492: return $this->intersectTypes(static fn (Type $type): Type => $type->toBitwiseNotType());
1493: }
1494:
1495: public function toGetClassResultType(): Type
1496: {
1497: return $this->intersectTypes(static fn (Type $type): Type => $type->toGetClassResultType());
1498: }
1499:
1500: public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
1501: {
1502: return $this->intersectTypes(static fn (Type $type): Type => $type->toClassConstantType($reflectionProvider));
1503: }
1504:
1505: public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
1506: {
1507: $types = [];
1508: $uncertainty = false;
1509: foreach ($this->getTypes() as $innerType) {
1510: $result = $innerType->toObjectTypeForInstanceofCheck();
1511: $types[] = $result->type;
1512: if (!$result->uncertainty) {
1513: continue;
1514: }
1515:
1516: $uncertainty = true;
1517: }
1518:
1519: return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty);
1520: }
1521:
1522: public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
1523: {
1524: $types = [];
1525: $uncertainty = false;
1526: foreach ($this->getTypes() as $innerType) {
1527: $result = $innerType->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass);
1528: $types[] = $result->type;
1529: if (!$result->uncertainty) {
1530: continue;
1531: }
1532:
1533: $uncertainty = true;
1534: }
1535:
1536: return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty);
1537: }
1538:
1539: public function toAbsoluteNumber(): Type
1540: {
1541: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());
1542:
1543: return $type;
1544: }
1545:
1546: public function toString(): Type
1547: {
1548: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString());
1549:
1550: return $type;
1551: }
1552:
1553: public function toInteger(): Type
1554: {
1555: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toInteger());
1556:
1557: return $type;
1558: }
1559:
1560: public function toFloat(): Type
1561: {
1562: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toFloat());
1563:
1564: return $type;
1565: }
1566:
1567: public function toArray(): Type
1568: {
1569: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toArray());
1570:
1571: return $type;
1572: }
1573:
1574: public function toArrayKey(): Type
1575: {
1576: if ($this->isDecimalIntegerString()->yes()) {
1577: return new IntegerType();
1578: }
1579:
1580: if ($this->isNumericString()->yes()) {
1581: return TypeCombinator::union(
1582: new IntegerType(),
1583: $this,
1584: );
1585: }
1586:
1587: if ($this->isString()->yes()) {
1588: return $this;
1589: }
1590:
1591: return $this->intersectTypes(static fn (Type $type): Type => $type->toArrayKey());
1592: }
1593:
1594: public function toCoercedArgumentType(bool $strictTypes): Type
1595: {
1596: return $this->intersectTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes));
1597: }
1598:
1599: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1600: {
1601: $types = TemplateTypeMap::createEmpty();
1602:
1603: foreach ($this->types as $type) {
1604: $types = $types->intersect($type->inferTemplateTypes($receivedType));
1605: }
1606:
1607: return $types;
1608: }
1609:
1610: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1611: {
1612: $references = [];
1613:
1614: foreach ($this->types as $type) {
1615: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
1616: $references[] = $reference;
1617: }
1618: }
1619:
1620: return $references;
1621: }
1622:
1623: public function traverse(callable $cb): Type
1624: {
1625: $types = [];
1626: $changed = false;
1627:
1628: foreach ($this->types as $type) {
1629: $newType = $cb($type);
1630: if ($type !== $newType) {
1631: $changed = true;
1632: }
1633: $types[] = $newType;
1634: }
1635:
1636: if ($changed) {
1637: $result = $types[0];
1638: for ($i = 1, $count = count($types); $i < $count; $i++) {
1639: $result = TypeCombinator::intersect($result, $types[$i]);
1640: }
1641: return $result;
1642: }
1643:
1644: return $this;
1645: }
1646:
1647: public function traverseSimultaneously(Type $right, callable $cb): Type
1648: {
1649: if ($this->isArray()->yes() && $right->isArray()->yes()) {
1650: $changed = false;
1651: $newTypes = [];
1652:
1653: foreach ($this->types as $innerType) {
1654: $newKeyType = $cb($innerType->getIterableKeyType(), $right->getIterableKeyType());
1655: $newValueType = $cb($innerType->getIterableValueType(), $right->getIterableValueType());
1656: if ($newKeyType === $innerType->getIterableKeyType() && $newValueType === $innerType->getIterableValueType()) {
1657: $newTypes[] = $innerType;
1658: continue;
1659: }
1660:
1661: $changed = true;
1662: $newTypes[] = TypeTraverser::map($innerType, static function (Type $type, callable $traverse) use ($innerType, $newKeyType, $newValueType): Type {
1663: if ($type === $innerType->getIterableKeyType()) {
1664: return $newKeyType;
1665: }
1666: if ($type === $innerType->getIterableValueType()) {
1667: return $newValueType;
1668: }
1669:
1670: return $traverse($type);
1671: });
1672: }
1673:
1674: if (!$changed) {
1675: return $this;
1676: }
1677:
1678: $result = $newTypes[0];
1679: for ($i = 1, $count = count($newTypes); $i < $count; $i++) {
1680: $result = TypeCombinator::intersect($result, $newTypes[$i]);
1681: }
1682: return $result;
1683: }
1684:
1685: return $this;
1686: }
1687:
1688: public function tryRemove(Type $typeToRemove): ?Type
1689: {
1690: return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1691: }
1692:
1693: public function exponentiate(Type $exponent): Type
1694: {
1695: return $this->intersectTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1696: }
1697:
1698: public function getFiniteTypes(): array
1699: {
1700: $compare = [];
1701: foreach ($this->types as $type) {
1702: $oneType = [];
1703: foreach ($type->getFiniteTypes() as $finiteType) {
1704: if ($finiteType instanceof EnumCaseObjectType) {
1705: $oneType[$finiteType->getClassName() . '::' . $finiteType->getEnumCaseName()] = $finiteType;
1706: continue;
1707: }
1708: $oneType[$finiteType->describe(VerbosityLevel::typeOnly())] = $finiteType;
1709: }
1710: $compare[] = $oneType;
1711: }
1712:
1713: $result = array_values(array_intersect_key(...$compare));
1714:
1715: if (count($result) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1716: return [];
1717: }
1718:
1719: return $result;
1720: }
1721:
1722: /**
1723: * @param callable(Type $type): TrinaryLogic $getResult
1724: * @param (callable(Type $type): bool)|null $filter
1725: */
1726: private function intersectResults(
1727: callable $getResult,
1728: ?callable $filter = null,
1729: ): TrinaryLogic
1730: {
1731: $types = $this->types;
1732: if ($filter !== null) {
1733: $types = array_filter($types, $filter);
1734: }
1735: if (count($types) === 0) {
1736: return TrinaryLogic::createNo();
1737: }
1738:
1739: return TrinaryLogic::lazyMaxMin($types, $getResult);
1740: }
1741:
1742: /**
1743: * @param callable(Type $type): Type $getType
1744: */
1745: private function intersectTypes(callable $getType): Type
1746: {
1747: $operands = array_map($getType, $this->types);
1748: $result = $operands[0];
1749: for ($i = 1, $count = count($operands); $i < $count; $i++) {
1750: $result = TypeCombinator::intersect($result, $operands[$i]);
1751: }
1752: return $result;
1753: }
1754:
1755: /**
1756: * @param callable(Type $type): Type $getType
1757: */
1758: private function intersectTypesPreserveTemplateType(callable $getType): Type
1759: {
1760: return $this->intersectTypes(static function (Type $type) use ($getType): Type {
1761: if ($type instanceof TemplateType) {
1762: return $type;
1763: }
1764: return $getType($type);
1765: });
1766: }
1767:
1768: public function toPhpDocNode(): TypeNode
1769: {
1770: $baseTypes = [];
1771: $typesToDescribe = [];
1772: $skipTypeNames = [];
1773:
1774: $nonEmptyStr = false;
1775: $nonFalsyStr = false;
1776: $isList = $this->isList()->yes();
1777: $isArray = $this->isArray()->yes();
1778: $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
1779: $describedTypes = [];
1780:
1781: foreach ($this->getSortedTypes() as $i => $type) {
1782: if ($type instanceof AccessoryNonEmptyStringType
1783: || $type instanceof AccessoryLiteralStringType
1784: || $type instanceof AccessoryNumericStringType
1785: || $type instanceof AccessoryNonFalsyStringType
1786: || $type instanceof AccessoryLowercaseStringType
1787: || $type instanceof AccessoryUppercaseStringType
1788: || $type instanceof AccessoryDecimalIntegerStringType
1789: ) {
1790: if ($type instanceof AccessoryNonFalsyStringType) {
1791: $nonFalsyStr = true;
1792: }
1793: if ($type instanceof AccessoryNonEmptyStringType) {
1794: $nonEmptyStr = true;
1795: }
1796: if ($nonEmptyStr && $nonFalsyStr) {
1797: // prevent redundant 'non-empty-string&non-falsy-string'
1798: foreach ($typesToDescribe as $key => $typeToDescribe) {
1799: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
1800: continue;
1801: }
1802:
1803: unset($typesToDescribe[$key]);
1804: }
1805: }
1806:
1807: $typesToDescribe[$i] = $type;
1808: $skipTypeNames[] = 'string';
1809: continue;
1810: }
1811:
1812: if ($isList || $isArray) {
1813: if ($type instanceof ArrayType) {
1814: $keyType = $type->getKeyType();
1815: $valueType = $type->getItemType();
1816: if ($isList) {
1817: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1818: $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-list' : 'list');
1819: if (!$isMixedValueType) {
1820: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1821: $valueType->toPhpDocNode(),
1822: ]);
1823: } else {
1824: $describedTypes[$i] = $identifierTypeNode;
1825: }
1826: } else {
1827: $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
1828: $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
1829: $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-array' : 'array');
1830: if (!$isMixedKeyType) {
1831: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1832: $keyType->toPhpDocNode(),
1833: $valueType->toPhpDocNode(),
1834: ]);
1835: } elseif (!$isMixedValueType) {
1836: $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
1837: $valueType->toPhpDocNode(),
1838: ]);
1839: } else {
1840: $describedTypes[$i] = $identifierTypeNode;
1841: }
1842: }
1843: continue;
1844: } elseif ($type instanceof ConstantArrayType) {
1845: $constantArrayTypeNode = $type->toPhpDocNode();
1846: if ($constantArrayTypeNode instanceof ArrayShapeNode) {
1847: $newKind = $constantArrayTypeNode->kind;
1848: if ($isList) {
1849: if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1850: $newKind = ArrayShapeNode::KIND_NON_EMPTY_LIST;
1851: } else {
1852: $newKind = ArrayShapeNode::KIND_LIST;
1853: }
1854: } elseif ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
1855: $newKind = ArrayShapeNode::KIND_NON_EMPTY_ARRAY;
1856: }
1857:
1858: if ($newKind !== $constantArrayTypeNode->kind) {
1859: if ($constantArrayTypeNode->sealed) {
1860: $constantArrayTypeNode = ArrayShapeNode::createSealed($constantArrayTypeNode->items, $newKind);
1861: } else {
1862: $constantArrayTypeNode = ArrayShapeNode::createUnsealed($constantArrayTypeNode->items, $constantArrayTypeNode->unsealedType, $newKind);
1863: }
1864: }
1865:
1866: $describedTypes[$i] = $constantArrayTypeNode;
1867: continue;
1868: }
1869: }
1870: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
1871: continue;
1872: }
1873: }
1874:
1875: if (!$type instanceof AccessoryType) {
1876: $baseTypes[$i] = $type;
1877: continue;
1878: }
1879:
1880: $accessoryPhpDocNode = $type->toPhpDocNode();
1881: if ($accessoryPhpDocNode instanceof IdentifierTypeNode && $accessoryPhpDocNode->name === '') {
1882: continue;
1883: }
1884:
1885: $typesToDescribe[$i] = $type;
1886: }
1887:
1888: foreach ($baseTypes as $i => $type) {
1889: $typeNode = $type->toPhpDocNode();
1890: if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') {
1891: $nonEmpty = false;
1892: $typeName = 'array';
1893: foreach ($typesToDescribe as $j => $typeToDescribe) {
1894: if ($typeToDescribe instanceof AccessoryArrayListType) {
1895: $typeName = 'list';
1896: if (count($typeNode->genericTypes) > 1) {
1897: array_shift($typeNode->genericTypes);
1898: }
1899: } elseif ($typeToDescribe instanceof NonEmptyArrayType) {
1900: $nonEmpty = true;
1901: } else {
1902: continue;
1903: }
1904:
1905: unset($typesToDescribe[$j]);
1906: }
1907:
1908: if ($nonEmpty) {
1909: $typeName = 'non-empty-' . $typeName;
1910: }
1911:
1912: $describedTypes[$i] = new GenericTypeNode(
1913: new IdentifierTypeNode($typeName),
1914: $typeNode->genericTypes,
1915: );
1916: continue;
1917: }
1918:
1919: if ($typeNode instanceof IdentifierTypeNode && in_array($typeNode->name, $skipTypeNames, true)) {
1920: continue;
1921: }
1922:
1923: $describedTypes[$i] = $typeNode;
1924: }
1925:
1926: foreach ($typesToDescribe as $i => $typeToDescribe) {
1927: $describedTypes[$i] = $typeToDescribe->toPhpDocNode();
1928: }
1929:
1930: ksort($describedTypes);
1931:
1932: $describedTypes = array_values($describedTypes);
1933:
1934: if (count($describedTypes) === 1) {
1935: return $describedTypes[0];
1936: }
1937:
1938: if (count($describedTypes) === 0) {
1939: throw new ShouldNotHappenException(sprintf('Intersection consists of %s but there should be at least one base type.', implode('&', array_map(static fn (Type $type) => $type->describe(VerbosityLevel::precise()), $this->types))));
1940: }
1941:
1942: return new IntersectionTypeNode($describedTypes);
1943: }
1944:
1945: public function hasTemplateOrLateResolvableType(): bool
1946: {
1947: foreach ($this->types as $type) {
1948: if (!$type->hasTemplateOrLateResolvableType()) {
1949: continue;
1950: }
1951:
1952: return true;
1953: }
1954:
1955: return false;
1956: }
1957:
1958: }
1959: