1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use DateTime;
6: use DateTimeImmutable;
7: use DateTimeInterface;
8: use Error;
9: use Exception;
10: use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
11: use PHPStan\Php\PhpVersion;
12: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
13: use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
14: use PHPStan\Reflection\ClassConstantReflection;
15: use PHPStan\Reflection\ClassMemberAccessAnswerer;
16: use PHPStan\Reflection\ExtendedMethodReflection;
17: use PHPStan\Reflection\ExtendedPropertyReflection;
18: use PHPStan\Reflection\InitializerExprTypeResolver;
19: use PHPStan\Reflection\MissingMethodFromReflectionException;
20: use PHPStan\Reflection\MissingPropertyFromReflectionException;
21: use PHPStan\Reflection\ReflectionProvider;
22: use PHPStan\Reflection\Type\UnionTypeUnresolvedMethodPrototypeReflection;
23: use PHPStan\Reflection\Type\UnionTypeUnresolvedPropertyPrototypeReflection;
24: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
25: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
26: use PHPStan\ShouldNotHappenException;
27: use PHPStan\TrinaryLogic;
28: use PHPStan\Turbo\ShadowedByTurboExtension;
29: use PHPStan\Type\Enum\EnumCaseObjectType;
30: use PHPStan\Type\Generic\AbsorbedTemplateArgumentType;
31: use PHPStan\Type\Generic\GenericClassStringType;
32: use PHPStan\Type\Generic\TemplateIterableType;
33: use PHPStan\Type\Generic\TemplateMixedType;
34: use PHPStan\Type\Generic\TemplateType;
35: use PHPStan\Type\Generic\TemplateTypeMap;
36: use PHPStan\Type\Generic\TemplateTypeVariance;
37: use PHPStan\Type\Generic\TemplateUnionType;
38: use PHPStan\Type\Generic\UnresolvedTemplateArgumentType;
39: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
40: use Throwable;
41: use function array_diff_assoc;
42: use function array_fill_keys;
43: use function array_intersect;
44: use function array_key_exists;
45: use function array_keys;
46: use function array_map;
47: use function array_merge;
48: use function array_slice;
49: use function array_unique;
50: use function array_values;
51: use function count;
52: use function implode;
53: use function sprintf;
54: use function str_contains;
55:
56: /** @api */
57: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/UnionType.cpp')]
58: class UnionType implements CompoundType
59: {
60:
61: use NonGeneralizableTypeTrait;
62:
63: public const EQUAL_UNION_CLASSES = [
64: DateTimeInterface::class => [DateTimeImmutable::class, DateTime::class],
65: Throwable::class => [Error::class, Exception::class], // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException
66: ];
67:
68: /**
69: * Sorting must not reorder $types: describe() sorts for readability, but $types is the
70: * type's value — getTypes() exposes it and callers merge array shapes in that order. It
71: * used to be sorted in place, so describing a union permanently changed what getTypes()
72: * returned, making an immutable value object's observable state depend on what had been
73: * called on it before.
74: *
75: * @var list<Type>|null
76: */
77: private ?array $sortedTypesCache = null;
78:
79: /** @var array<int, string> */
80: private array $cachedDescriptions = [];
81:
82: /**
83: * Identity-keyed view of $types, built on first use. False once it is known that the
84: * members cannot be keyed at all, so a union of objects pays for the attempt only once.
85: */
86: private FiniteTypeSet|false|null $finiteTypeSet = null;
87:
88: /** @var list<Type>|null */
89: private ?array $finiteTypes = null;
90:
91: private ?TrinaryLogic $isNull = null;
92:
93: private ?TrinaryLogic $isCallable = null;
94:
95: /**
96: * @api
97: * @param list<Type> $types
98: */
99: public function __construct(private array $types, private bool $normalized = false)
100: {
101: $throwException = static function () use ($types): void {
102: throw new ShouldNotHappenException(sprintf(
103: 'Cannot create %s with: %s',
104: self::class,
105: implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)),
106: ));
107: };
108: if (count($types) < 2) {
109: $throwException();
110: }
111: foreach ($types as $type) {
112: if (!($type instanceof UnionType)) {
113: continue;
114: }
115: if ($type instanceof TemplateType) {
116: continue;
117: }
118:
119: $throwException();
120: }
121: }
122:
123: /**
124: * @return list<Type>
125: */
126: public function getTypes(): array
127: {
128: return $this->types;
129: }
130:
131: /**
132: * @param callable(Type $type): bool $filterCb
133: */
134: public function filterTypes(callable $filterCb): Type
135: {
136: $newTypes = [];
137: $changed = false;
138: foreach ($this->getTypes() as $innerType) {
139: if (!$filterCb($innerType)) {
140: $changed = true;
141: continue;
142: }
143:
144: $newTypes[] = $innerType;
145: }
146:
147: if (!$changed) {
148: return $this;
149: }
150:
151: return TypeCombinator::union(...$newTypes);
152: }
153:
154: public function isNormalized(): bool
155: {
156: return $this->normalized;
157: }
158:
159: public function getFiniteTypeSet(): ?FiniteTypeSet
160: {
161: $finiteTypeSet = $this->finiteTypeSet ??= FiniteTypeSet::create($this->types) ?? false;
162: if ($finiteTypeSet === false) {
163: return null;
164: }
165:
166: return $finiteTypeSet;
167: }
168:
169: /**
170: * @return list<Type>
171: */
172: protected function getSortedTypes(): array
173: {
174: return $this->sortedTypesCache ??= UnionTypeHelper::sortTypes($this->types);
175: }
176:
177: public function getReferencedClasses(): array
178: {
179: $classes = [];
180: foreach ($this->types as $type) {
181: foreach ($type->getReferencedClasses() as $className) {
182: $classes[] = $className;
183: }
184: }
185:
186: return $classes;
187: }
188:
189: public function getObjectClassNames(): array
190: {
191: return array_values(array_unique($this->pickFromTypes(
192: static fn (Type $type) => $type->getObjectClassNames(),
193: static fn (Type $type) => $type->isObject()->yes(),
194: )));
195: }
196:
197: public function getObjectClassReflections(): array
198: {
199: return $this->pickFromTypes(
200: static fn (Type $type) => $type->getObjectClassReflections(),
201: static fn (Type $type) => $type->isObject()->yes(),
202: );
203: }
204:
205: public function getArrays(): array
206: {
207: return $this->pickFromTypes(
208: static fn (Type $type) => $type->getArrays(),
209: static fn (Type $type) => $type->isArray()->yes(),
210: );
211: }
212:
213: public function getConstantArrays(): array
214: {
215: return $this->pickFromTypes(
216: static fn (Type $type) => $type->getConstantArrays(),
217: static fn (Type $type) => $type->isArray()->yes(),
218: );
219: }
220:
221: public function getConstantStrings(): array
222: {
223: return $this->pickFromTypes(
224: static fn (Type $type) => $type->getConstantStrings(),
225: static fn (Type $type) => $type->isString()->yes(),
226: );
227: }
228:
229: public function accepts(Type $type, bool $strictTypes): AcceptsResult
230: {
231: $finiteTypeSet = $this->getFiniteTypeSet();
232: if ($finiteTypeSet !== null) {
233: $key = FiniteTypeSet::key($type);
234: if ($key !== null && $finiteTypeSet->has($key)) {
235: return AcceptsResult::createYes();
236: }
237:
238: if ($finiteTypeSet->isComplete()) {
239: if ($key !== null) {
240: // A value the union does not hold can still be accepted through scalar
241: // coercion, so unlike isSuperTypeOf() the answer is not simply no - but it
242: // is the same for every member of a kind, so one member of each is enough.
243: // None of the branches below apply to a value: it is not iterable, not
244: // compound, and an enum case already is the union of its own cases.
245: $result = AcceptsResult::createNo();
246: foreach ($finiteTypeSet->getRepresentativesOfOtherKinds($type) as $representative) {
247: $result = $result->or($representative->accepts($type, $strictTypes));
248: }
249:
250: return $result;
251: }
252:
253: if ($type instanceof self && !$type instanceof TemplateType) {
254: $otherFiniteTypeSet = $type->getFiniteTypeSet();
255: if ($otherFiniteTypeSet !== null && $otherFiniteTypeSet->isComplete()) {
256: // A member standing for a single value never accepts more than one of
257: // the other union's values, and the other union has at least two of
258: // them - so the member-by-member or() below cannot come out yes, and
259: // its result is discarded in favour of the compound answer anyway.
260: return $type->isAcceptedBy($this, $strictTypes);
261: }
262: }
263: }
264: }
265:
266: if ($type instanceof IterableType) {
267: return $this->accepts($type->toArrayOrTraversable(), $strictTypes);
268: }
269:
270: foreach (self::EQUAL_UNION_CLASSES as $baseClass => $classes) {
271: if (!$type->equals(new ObjectType($baseClass))) {
272: continue;
273: }
274:
275: $union = TypeCombinator::union(
276: ...array_map(static fn (string $objectClass): Type => new ObjectType($objectClass), $classes),
277: );
278: if ($this->accepts($union, $strictTypes)->yes()) {
279: return AcceptsResult::createYes();
280: }
281: break;
282: }
283:
284: $innerAccepts = [];
285: $result = AcceptsResult::createNo();
286: foreach ($this->getSortedTypes() as $i => $innerType) {
287: $innerResult = $innerType->accepts($type, $strictTypes);
288: $innerAccepts[$i] = $innerResult;
289: $result = $result->or($innerResult->decorateReasons(static fn (string $reason) => sprintf('Type #%d from the union: %s', $i + 1, $reason)));
290: }
291: if ($result->yes()) {
292: return $result;
293: }
294:
295: $commonReasons = null;
296: foreach ($innerAccepts as $innerResult) {
297: if ($commonReasons === null) {
298: $commonReasons = $innerResult->reasons;
299: continue;
300: }
301: $commonReasons = array_values(array_intersect($commonReasons, $innerResult->reasons));
302: }
303: if ($commonReasons !== null && count($commonReasons) > 0) {
304: $decorated = [];
305: foreach (array_keys($innerAccepts) as $i) {
306: foreach ($commonReasons as $reason) {
307: $decorated[] = sprintf('Type #%d from the union: %s', $i + 1, $reason);
308: }
309: }
310: $result = new AcceptsResult($result->result, $decorated);
311: }
312:
313: if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) {
314: return $type->isAcceptedBy($this, $strictTypes);
315: }
316:
317: if ($type instanceof TemplateUnionType) {
318: return $result->or($type->isAcceptedBy($this, $strictTypes));
319: }
320:
321: if ($type->isEnum()->yes() && !$this->isEnum()->no()) {
322: $enumCasesUnion = TypeCombinator::union(...$type->getEnumCases());
323: if (!$type->equals($enumCasesUnion)) {
324: return $this->accepts($enumCasesUnion, $strictTypes);
325: }
326: }
327:
328: return $result;
329: }
330:
331: public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult
332: {
333: if (
334: ($otherType instanceof self && !$otherType instanceof TemplateUnionType)
335: || ($otherType instanceof IterableType && !$otherType instanceof TemplateIterableType)
336: || $otherType instanceof NeverType
337: || $otherType instanceof IntegerRangeType
338: // stands for its delegate, which can itself be a union
339: || $otherType instanceof UnresolvedTemplateArgumentType
340: ) {
341: return $otherType->isSubTypeOf($this);
342: }
343:
344: $types = $this->types;
345: $finiteTypeSet = $this->getFiniteTypeSet();
346: if ($finiteTypeSet !== null) {
347: $key = FiniteTypeSet::key($otherType);
348: if ($key !== null) {
349: if ($finiteTypeSet->has($key)) {
350: return IsSuperTypeOfResult::createYes();
351: }
352:
353: // Every keyed member stands for a different value than $otherType, so all of
354: // them answer no - only the members that could not be keyed are left to ask.
355: if ($finiteTypeSet->isComplete()) {
356: return IsSuperTypeOfResult::createNo();
357: }
358:
359: $types = $finiteTypeSet->getOthers();
360: }
361: }
362:
363: $results = [];
364: foreach ($types as $innerType) {
365: $result = $innerType->isSuperTypeOf($otherType);
366: if ($result->yes()) {
367: return $result;
368: }
369: $results[] = $result;
370: }
371: $result = IsSuperTypeOfResult::createNo()->or(...$results);
372:
373: if (
374: $otherType instanceof TemplateUnionType
375: || ($otherType instanceof LateResolvableType && $otherType instanceof CompoundType && !$otherType instanceof TemplateType)
376: ) {
377: return $result->or($otherType->isSubTypeOf($this));
378: }
379:
380: return $result;
381: }
382:
383: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
384: {
385: $containment = $this->finiteTypeSetContainedIn($otherType, false);
386: if ($containment !== null) {
387: if ($containment->maybe()) {
388: return IsSuperTypeOfResult::createMaybe();
389: }
390:
391: return IsSuperTypeOfResult::createFromBoolean($containment->yes());
392: }
393:
394: return IsSuperTypeOfResult::extremeIdentity(...array_map(static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType), $this->types));
395: }
396:
397: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
398: {
399: // Unlike isSubTypeOf() only the positive answer holds: a member $acceptingType does
400: // not hold can still be accepted through scalar coercion.
401: $containment = $this->finiteTypeSetContainedIn($acceptingType, true);
402: if ($containment !== null && $containment->yes()) {
403: return AcceptsResult::createYes();
404: }
405:
406: return AcceptsResult::extremeIdentity(...array_map(static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), $this->types));
407: }
408:
409: /**
410: * Whether $otherType holds every member of this one, or null when the question cannot
411: * be settled from the identity maps alone.
412: *
413: * $otherType is compared as a set of values: a union through its own map, a single
414: * finite value as the one-member set holding it.
415: *
416: * $yesOnly skips the completeness requirement on the other union: a member missing from
417: * its map only rules out the yes answer, which is all the caller is after.
418: */
419: private function finiteTypeSetContainedIn(Type $otherType, bool $yesOnly): ?TrinaryLogic
420: {
421: $finiteTypeSet = $this->getFiniteTypeSet();
422: if ($finiteTypeSet === null || !$finiteTypeSet->isComplete()) {
423: return null;
424: }
425:
426: if (!$otherType instanceof self || $otherType instanceof TemplateType) {
427: // A single value covers a member iff they are the same value, and no member
428: // stands for a value it is not keyed by - so one lookup answers for every member
429: // at once. This is the shape TypeCombinator::remove() takes to ask whether
430: // removing a value empties a union of values, and the only way a non-union other
431: // type reaches this helper at all.
432: $key = FiniteTypeSet::key($otherType);
433: if ($key === null) {
434: return null;
435: }
436:
437: $containment = $finiteTypeSet->containedInKey($key);
438:
439: // No constant value accepts another one - not even a numeric string an int, in
440: // either direction - so today accepts() agrees with isSuperTypeOf() here and this
441: // guard never changes an answer. It is kept because that is a fact about the
442: // current value types rather than something the identity map guarantees: a value
443: // the map does not hold being accepted anyway is exactly what accepts() is
444: // allowed to do, and all isAcceptedBy() wants from the map is the yes.
445: if (!$containment->yes() && $yesOnly) {
446: return null;
447: }
448:
449: return $containment;
450: }
451:
452: $otherFiniteTypeSet = $otherType->getFiniteTypeSet();
453: if ($otherFiniteTypeSet === null) {
454: return null;
455: }
456:
457: $containment = $finiteTypeSet->containedIn($otherFiniteTypeSet);
458: if (!$containment->yes() && ($yesOnly || !$otherFiniteTypeSet->isComplete())) {
459: return null;
460: }
461:
462: return $containment;
463: }
464:
465: public function equals(Type $type): bool
466: {
467: if (!$type instanceof static) {
468: return false;
469: }
470:
471: if (count($this->types) !== count($type->types)) {
472: return false;
473: }
474:
475: $finiteTypeSet = $this->getFiniteTypeSet();
476: if ($finiteTypeSet !== null && $finiteTypeSet->isComplete()) {
477: $otherFiniteTypeSet = $type->getFiniteTypeSet();
478: if ($otherFiniteTypeSet !== null && $otherFiniteTypeSet->isComplete()) {
479: return $finiteTypeSet->containedIn($otherFiniteTypeSet)->yes();
480: }
481: }
482:
483: $otherTypes = $type->types;
484: foreach ($this->types as $innerType) {
485: $match = false;
486: foreach ($otherTypes as $i => $otherType) {
487: if (!$innerType->equals($otherType)) {
488: continue;
489: }
490:
491: $match = true;
492: unset($otherTypes[$i]);
493: break;
494: }
495:
496: if (!$match) {
497: return false;
498: }
499: }
500:
501: return count($otherTypes) === 0;
502: }
503:
504: public function describe(VerbosityLevel $level): string
505: {
506: if (isset($this->cachedDescriptions[$level->getLevelValue()])) {
507: return $this->cachedDescriptions[$level->getLevelValue()];
508: }
509: $joinTypes = static function (array $types) use ($level): string {
510: $typeNames = [];
511: foreach ($types as $i => $type) {
512: if ($type instanceof ClosureType || $type instanceof CallableType || $type instanceof TemplateUnionType) {
513: $typeNames[] = sprintf('(%s)', $type->describe($level));
514: } elseif ($type instanceof TemplateType) {
515: $isLast = $i >= count($types) - 1;
516: $bound = $type->getBound();
517: if (
518: !$isLast
519: && ($level->isTypeOnly() || $level->isValue())
520: && !($bound instanceof MixedType && $bound->getSubtractedType() === null && !$bound instanceof TemplateMixedType)
521: ) {
522: $typeNames[] = sprintf('(%s)', $type->describe($level));
523: } else {
524: $typeNames[] = $type->describe($level);
525: }
526: } elseif ($type instanceof IntersectionType) {
527: $intersectionDescription = $type->describe($level);
528: if (str_contains($intersectionDescription, '&')) {
529: $typeNames[] = sprintf('(%s)', $intersectionDescription);
530: } else {
531: $typeNames[] = $intersectionDescription;
532: }
533: } else {
534: $typeNames[] = $type->describe($level);
535: }
536: }
537:
538: if ($level->isPrecise() || $level->isCache()) {
539: $duplicates = array_diff_assoc($typeNames, array_unique($typeNames));
540: if (count($duplicates) > 0) {
541: $indexByDuplicate = array_fill_keys($duplicates, 0);
542: foreach ($typeNames as $key => $typeName) {
543: if (!isset($indexByDuplicate[$typeName])) {
544: continue;
545: }
546:
547: $typeNames[$key] = $typeName . '#' . ++$indexByDuplicate[$typeName];
548: }
549: }
550: } else {
551: $typeNames = array_unique($typeNames);
552: }
553:
554: if (count($typeNames) > 1024) {
555: return implode('|', array_slice($typeNames, 0, 1024)) . "|\u{2026}";
556: }
557:
558: return implode('|', $typeNames);
559: };
560:
561: return $this->cachedDescriptions[$level->getLevelValue()] = $level->handle(
562: function () use ($joinTypes): string {
563: $types = TypeCombinator::union(...array_map(static function (Type $type): Type {
564: if (
565: $type->isConstantValue()->yes()
566: && $type->isTrue()->or($type->isFalse())->no()
567: ) {
568: return $type->generalize(GeneralizePrecision::lessSpecific());
569: }
570:
571: return $type;
572: }, $this->getSortedTypes()));
573:
574: if ($types instanceof UnionType) {
575: return $joinTypes($types->getSortedTypes());
576: }
577:
578: return $joinTypes([$types]);
579: },
580: fn (): string => $joinTypes($this->getSortedTypes()),
581: );
582: }
583:
584: /**
585: * @param callable(Type $type): TrinaryLogic $canCallback
586: * @param callable(Type $type): TrinaryLogic $hasCallback
587: */
588: private function hasInternal(
589: callable $canCallback,
590: callable $hasCallback,
591: ): TrinaryLogic
592: {
593: return TrinaryLogic::lazyExtremeIdentity($this->types, static function (Type $type) use ($canCallback, $hasCallback): TrinaryLogic {
594: if ($canCallback($type)->no()) {
595: return TrinaryLogic::createNo();
596: }
597:
598: return $hasCallback($type);
599: });
600: }
601:
602: /**
603: * @template TObject of object
604: * @param callable(Type $type): TrinaryLogic $hasCallback
605: * @param callable(Type $type): TObject $getCallback
606: * @return TObject
607: */
608: private function getInternal(
609: callable $hasCallback,
610: callable $getCallback,
611: ): object
612: {
613: /** @var TrinaryLogic|null $result */
614: $result = null;
615:
616: /** @var TObject|null $object */
617: $object = null;
618: foreach ($this->types as $type) {
619: $has = $hasCallback($type);
620: if (!$has->yes()) {
621: continue;
622: }
623: if ($result !== null && $result->compareTo($has) !== $has) {
624: continue;
625: }
626:
627: $get = $getCallback($type);
628: $result = $has;
629: $object = $get;
630: }
631:
632: if ($object === null) {
633: throw new ShouldNotHappenException();
634: }
635:
636: return $object;
637: }
638:
639: public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type
640: {
641: return $this->unionTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName));
642: }
643:
644: public function isObject(): TrinaryLogic
645: {
646: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isObject());
647: }
648:
649: public function getClassStringType(): Type
650: {
651: return $this->unionTypes(static fn (Type $type): Type => $type->getClassStringType());
652: }
653:
654: public function isEnum(): TrinaryLogic
655: {
656: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isEnum());
657: }
658:
659: public function canAccessProperties(): TrinaryLogic
660: {
661: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
662: }
663:
664: public function hasProperty(string $propertyName): TrinaryLogic
665: {
666: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
667: }
668:
669: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
670: {
671: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
672: }
673:
674: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
675: {
676: $propertyPrototypes = [];
677: foreach ($this->types as $type) {
678: if (!$type->hasProperty($propertyName)->yes()) {
679: continue;
680: }
681:
682: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
683: }
684:
685: $propertiesCount = count($propertyPrototypes);
686: if ($propertiesCount === 0) {
687: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
688: }
689:
690: if ($propertiesCount === 1) {
691: return $propertyPrototypes[0];
692: }
693:
694: return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
695: }
696:
697: public function hasInstanceProperty(string $propertyName): TrinaryLogic
698: {
699: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName));
700: }
701:
702: public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
703: {
704: return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty();
705: }
706:
707: public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
708: {
709: $propertyPrototypes = [];
710: foreach ($this->types as $type) {
711: if (!$type->hasInstanceProperty($propertyName)->yes()) {
712: continue;
713: }
714:
715: $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this);
716: }
717:
718: $propertiesCount = count($propertyPrototypes);
719: if ($propertiesCount === 0) {
720: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
721: }
722:
723: if ($propertiesCount === 1) {
724: return $propertyPrototypes[0];
725: }
726:
727: return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
728: }
729:
730: public function hasStaticProperty(string $propertyName): TrinaryLogic
731: {
732: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName));
733: }
734:
735: public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
736: {
737: return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty();
738: }
739:
740: public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
741: {
742: $propertyPrototypes = [];
743: foreach ($this->types as $type) {
744: if (!$type->hasStaticProperty($propertyName)->yes()) {
745: continue;
746: }
747:
748: $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
749: }
750:
751: $propertiesCount = count($propertyPrototypes);
752: if ($propertiesCount === 0) {
753: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
754: }
755:
756: if ($propertiesCount === 1) {
757: return $propertyPrototypes[0];
758: }
759:
760: return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
761: }
762:
763: public function canCallMethods(): TrinaryLogic
764: {
765: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
766: }
767:
768: public function hasMethod(string $methodName): TrinaryLogic
769: {
770: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
771: }
772:
773: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
774: {
775: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
776: }
777:
778: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
779: {
780: $methodPrototypes = [];
781: foreach ($this->types as $type) {
782: if (!$type->hasMethod($methodName)->yes()) {
783: continue;
784: }
785:
786: $prototype = $type->getUnresolvedMethodPrototype($methodName, $scope);
787: if ($this instanceof TemplateType) {
788: $prototype = $prototype->withCalledOnType($this);
789: }
790: $methodPrototypes[] = $prototype;
791: }
792:
793: $methodsCount = count($methodPrototypes);
794: if ($methodsCount === 0) {
795: throw new MissingMethodFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $methodName);
796: }
797:
798: if ($methodsCount === 1) {
799: return $methodPrototypes[0];
800: }
801:
802: return new UnionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
803: }
804:
805: public function canAccessConstants(): TrinaryLogic
806: {
807: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
808: }
809:
810: public function hasConstant(string $constantName): TrinaryLogic
811: {
812: return $this->hasInternal(
813: static fn (Type $type): TrinaryLogic => $type->canAccessConstants(),
814: static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName),
815: );
816: }
817:
818: public function getConstant(string $constantName): ClassConstantReflection
819: {
820: return $this->getInternal(
821: static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName),
822: static fn (Type $type): ClassConstantReflection => $type->getConstant($constantName),
823: );
824: }
825:
826: public function isIterable(): TrinaryLogic
827: {
828: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
829: }
830:
831: public function isIterableAtLeastOnce(): TrinaryLogic
832: {
833: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce());
834: }
835:
836: public function getArraySize(): Type
837: {
838: return $this->unionTypes(static fn (Type $type): Type => $type->getArraySize());
839: }
840:
841: public function getIterableKeyType(): Type
842: {
843: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
844: }
845:
846: public function getFirstIterableKeyType(): Type
847: {
848: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
849: }
850:
851: public function getLastIterableKeyType(): Type
852: {
853: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
854: }
855:
856: public function getIterableValueType(): Type
857: {
858: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
859: }
860:
861: public function getFirstIterableValueType(): Type
862: {
863: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
864: }
865:
866: public function getLastIterableValueType(): Type
867: {
868: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
869: }
870:
871: public function isArray(): TrinaryLogic
872: {
873: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isArray());
874: }
875:
876: public function isConstantArray(): TrinaryLogic
877: {
878: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray());
879: }
880:
881: public function isOversizedArray(): TrinaryLogic
882: {
883: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
884: }
885:
886: public function isList(): TrinaryLogic
887: {
888: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isList());
889: }
890:
891: public function isString(): TrinaryLogic
892: {
893: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isString());
894: }
895:
896: public function isNumericString(): TrinaryLogic
897: {
898: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
899: }
900:
901: public function isDecimalIntegerString(): TrinaryLogic
902: {
903: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isDecimalIntegerString());
904: }
905:
906: public function isNonEmptyString(): TrinaryLogic
907: {
908: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
909: }
910:
911: public function isNonFalsyString(): TrinaryLogic
912: {
913: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
914: }
915:
916: public function isLiteralString(): TrinaryLogic
917: {
918: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
919: }
920:
921: public function isLowercaseString(): TrinaryLogic
922: {
923: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString());
924: }
925:
926: public function isUppercaseString(): TrinaryLogic
927: {
928: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString());
929: }
930:
931: public function isClassString(): TrinaryLogic
932: {
933: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassString());
934: }
935:
936: public function getClassStringObjectType(): Type
937: {
938: return $this->unionTypes(static fn (Type $type): Type => $type->getClassStringObjectType());
939: }
940:
941: public function getObjectTypeOrClassStringObjectType(): Type
942: {
943: return $this->unionTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType());
944: }
945:
946: public function isVoid(): TrinaryLogic
947: {
948: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isVoid());
949: }
950:
951: public function isScalar(): TrinaryLogic
952: {
953: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isScalar());
954: }
955:
956: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
957: {
958: return $this->notBenevolentUnionResults(
959: static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic(),
960: )->toBooleanType();
961: }
962:
963: public function isOffsetAccessible(): TrinaryLogic
964: {
965: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
966: }
967:
968: public function isOffsetAccessLegal(): TrinaryLogic
969: {
970: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
971: }
972:
973: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
974: {
975: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
976: }
977:
978: public function getOffsetValueType(Type $offsetType): Type
979: {
980: $types = [];
981: foreach ($this->types as $innerType) {
982: $valueType = $innerType->getOffsetValueType($offsetType);
983: if ($valueType instanceof ErrorType) {
984: continue;
985: }
986:
987: $types[] = $valueType;
988: }
989:
990: if (count($types) === 0) {
991: return new ErrorType();
992: }
993:
994: return TypeCombinator::union(...$types);
995: }
996:
997: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
998: {
999: return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
1000: }
1001:
1002: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
1003: {
1004: return $this->unionTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
1005: }
1006:
1007: public function unsetOffset(Type $offsetType): Type
1008: {
1009: return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
1010: }
1011:
1012: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
1013: {
1014: return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict));
1015: }
1016:
1017: public function getKeysArray(): Type
1018: {
1019: return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArray());
1020: }
1021:
1022: public function getValuesArray(): Type
1023: {
1024: return $this->unionTypes(static fn (Type $type): Type => $type->getValuesArray());
1025: }
1026:
1027: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
1028: {
1029: return $this->unionTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
1030: }
1031:
1032: public function fillKeysArray(Type $valueType): Type
1033: {
1034: return $this->unionTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
1035: }
1036:
1037: public function flipArray(): Type
1038: {
1039: return $this->unionTypes(static fn (Type $type): Type => $type->flipArray());
1040: }
1041:
1042: public function intersectKeyArray(Type $otherArraysType): Type
1043: {
1044: return $this->unionTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
1045: }
1046:
1047: public function popArray(): Type
1048: {
1049: return $this->unionTypes(static fn (Type $type): Type => $type->popArray());
1050: }
1051:
1052: public function reverseArray(TrinaryLogic $preserveKeys): Type
1053: {
1054: return $this->unionTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys));
1055: }
1056:
1057: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
1058: {
1059: return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict));
1060: }
1061:
1062: public function shiftArray(): Type
1063: {
1064: return $this->unionTypes(static fn (Type $type): Type => $type->shiftArray());
1065: }
1066:
1067: public function shuffleArray(): Type
1068: {
1069: return $this->unionTypes(static fn (Type $type): Type => $type->shuffleArray());
1070: }
1071:
1072: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
1073: {
1074: return $this->unionTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
1075: }
1076:
1077: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1078: {
1079: return $this->unionTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
1080: }
1081:
1082: public function truncateListToSize(Type $sizeType): Type
1083: {
1084: return $this->unionTypes(static fn (Type $type): Type => $type->truncateListToSize($sizeType));
1085: }
1086:
1087: public function makeListMaybe(): Type
1088: {
1089: return $this->unionTypes(static fn (Type $type): Type => $type->makeListMaybe());
1090: }
1091:
1092: public function mapValueType(callable $cb): Type
1093: {
1094: return $this->unionTypes(static fn (Type $type): Type => $type->mapValueType($cb));
1095: }
1096:
1097: public function mapKeyType(callable $cb): Type
1098: {
1099: return $this->unionTypes(static fn (Type $type): Type => $type->mapKeyType($cb));
1100: }
1101:
1102: public function makeAllArrayKeysOptional(): Type
1103: {
1104: return $this->unionTypes(static fn (Type $type): Type => $type->makeAllArrayKeysOptional());
1105: }
1106:
1107: public function changeKeyCaseArray(?int $case): Type
1108: {
1109: return $this->unionTypes(static fn (Type $type): Type => $type->changeKeyCaseArray($case));
1110: }
1111:
1112: public function filterArrayRemovingFalsey(): Type
1113: {
1114: return $this->unionTypes(static fn (Type $type): Type => $type->filterArrayRemovingFalsey());
1115: }
1116:
1117: public function getEnumCases(): array
1118: {
1119: return $this->pickFromTypes(
1120: static fn (Type $type) => $type->getEnumCases(),
1121: static fn (Type $type) => $type->isObject()->yes(),
1122: );
1123: }
1124:
1125: public function getEnumCaseObject(): ?EnumCaseObjectType
1126: {
1127: $cases = $this->getEnumCases();
1128:
1129: if (count($cases) === 1) {
1130: return $cases[0];
1131: }
1132:
1133: return null;
1134: }
1135:
1136: public function isCallable(): TrinaryLogic
1137: {
1138: return $this->isCallable ??= $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
1139: }
1140:
1141: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
1142: {
1143: $acceptors = [];
1144:
1145: foreach ($this->types as $type) {
1146: if ($type->isCallable()->no()) {
1147: continue;
1148: }
1149:
1150: $acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope));
1151: }
1152:
1153: if (count($acceptors) === 0) {
1154: throw new ShouldNotHappenException();
1155: }
1156:
1157: return $acceptors;
1158: }
1159:
1160: public function isCloneable(): TrinaryLogic
1161: {
1162: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
1163: }
1164:
1165: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1166: {
1167: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion));
1168: }
1169:
1170: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1171: {
1172: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion));
1173: }
1174:
1175: public function isNull(): TrinaryLogic
1176: {
1177: return $this->isNull ??= $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNull());
1178: }
1179:
1180: public function isConstantValue(): TrinaryLogic
1181: {
1182: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
1183: }
1184:
1185: public function isConstantScalarValue(): TrinaryLogic
1186: {
1187: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
1188: }
1189:
1190: public function getConstantScalarTypes(): array
1191: {
1192: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarTypes());
1193: }
1194:
1195: public function getConstantScalarValues(): array
1196: {
1197: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarValues());
1198: }
1199:
1200: public function isTrue(): TrinaryLogic
1201: {
1202: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
1203: }
1204:
1205: public function isFalse(): TrinaryLogic
1206: {
1207: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
1208: }
1209:
1210: public function isBoolean(): TrinaryLogic
1211: {
1212: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
1213: }
1214:
1215: public function isFloat(): TrinaryLogic
1216: {
1217: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
1218: }
1219:
1220: public function isInteger(): TrinaryLogic
1221: {
1222: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
1223: }
1224:
1225: public function getSmallerType(PhpVersion $phpVersion): Type
1226: {
1227: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion));
1228: }
1229:
1230: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
1231: {
1232: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion));
1233: }
1234:
1235: public function getGreaterType(PhpVersion $phpVersion): Type
1236: {
1237: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion));
1238: }
1239:
1240: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
1241: {
1242: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion));
1243: }
1244:
1245: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1246: {
1247: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion));
1248: }
1249:
1250: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1251: {
1252: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion));
1253: }
1254:
1255: public function toBoolean(): BooleanType
1256: {
1257: /** @var BooleanType $type */
1258: $type = $this->unionTypes(static fn (Type $type): BooleanType => $type->toBoolean());
1259:
1260: return $type;
1261: }
1262:
1263: public function toNumber(): Type
1264: {
1265: $type = $this->unionTypes(static fn (Type $type): Type => $type->toNumber());
1266:
1267: return $type;
1268: }
1269:
1270: public function toBitwiseNotType(): Type
1271: {
1272: return $this->unionTypes(static fn (Type $type): Type => $type->toBitwiseNotType());
1273: }
1274:
1275: public function toGetClassResultType(): Type
1276: {
1277: return $this->unionTypes(static fn (Type $type): Type => $type->toGetClassResultType());
1278: }
1279:
1280: public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
1281: {
1282: return $this->unionTypes(static fn (Type $type): Type => $type->toClassConstantType($reflectionProvider));
1283: }
1284:
1285: public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
1286: {
1287: $types = [];
1288: $uncertainty = false;
1289: foreach ($this->getTypes() as $innerType) {
1290: $result = $innerType->toObjectTypeForInstanceofCheck();
1291: $types[] = $result->type;
1292: if (!$result->uncertainty) {
1293: continue;
1294: }
1295:
1296: $uncertainty = true;
1297: }
1298:
1299: return new ClassNameToObjectTypeResult(TypeCombinator::union(...$types), $uncertainty);
1300: }
1301:
1302: public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
1303: {
1304: $types = [];
1305: $uncertainty = false;
1306: foreach ($this->getTypes() as $innerType) {
1307: $result = $innerType->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass);
1308: $types[] = $result->type;
1309: if (!$result->uncertainty) {
1310: continue;
1311: }
1312:
1313: $uncertainty = true;
1314: }
1315:
1316: return new ClassNameToObjectTypeResult(TypeCombinator::union(...$types), $uncertainty);
1317: }
1318:
1319: public function toAbsoluteNumber(): Type
1320: {
1321: $type = $this->unionTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());
1322:
1323: return $type;
1324: }
1325:
1326: public function toString(): Type
1327: {
1328: $type = $this->unionTypes(static fn (Type $type): Type => $type->toString());
1329:
1330: return $type;
1331: }
1332:
1333: public function toInteger(): Type
1334: {
1335: $type = $this->unionTypes(static fn (Type $type): Type => $type->toInteger());
1336:
1337: return $type;
1338: }
1339:
1340: public function toFloat(): Type
1341: {
1342: $type = $this->unionTypes(static fn (Type $type): Type => $type->toFloat());
1343:
1344: return $type;
1345: }
1346:
1347: public function toArray(): Type
1348: {
1349: $type = $this->unionTypes(static fn (Type $type): Type => $type->toArray());
1350:
1351: return $type;
1352: }
1353:
1354: public function toArrayKey(): Type
1355: {
1356: $level = ReportUnsafeArrayStringKeyCastingToggle::getLevel();
1357: if ($level !== ReportUnsafeArrayStringKeyCastingToggle::PREVENT || $this->isInteger()->no()) {
1358: return $this->unionTypes(static fn (Type $type): Type => $type->toArrayKey());
1359: }
1360:
1361: return $this->unionTypes(static function (Type $type): Type {
1362: if ($type instanceof StringType) { // @phpstan-ignore phpstanApi.instanceofType
1363: return $type;
1364: }
1365:
1366: return $type->toArrayKey();
1367: });
1368: }
1369:
1370: public function toCoercedArgumentType(bool $strictTypes): Type
1371: {
1372: return $this->unionTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes));
1373: }
1374:
1375: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1376: {
1377: if ($receivedType instanceof IterableType) {
1378: $receivedType = $receivedType->toArrayOrTraversable();
1379: }
1380:
1381: // A received type that one of the members definitely accepts is
1382: // absorbed by that member: it tells nothing about the other members,
1383: // so T|null receiving null must not bind T. Only what no member
1384: // absorbs is inferred against the remaining members below.
1385: // never is the exception: every member is its supertype, so which
1386: // member absorbs it is meaningless - it always has to be inferred.
1387: $types = TemplateTypeMap::createEmpty();
1388: $receivedTypes = $receivedType instanceof UnionType ? $receivedType->getTypes() : [$receivedType];
1389: $remainingReceivedTypes = [];
1390: foreach ($receivedTypes as $receivedInnerType) {
1391: if ($receivedInnerType instanceof NeverType) {
1392: $remainingReceivedTypes[] = $receivedInnerType;
1393: continue;
1394: }
1395: foreach ($this->types as $type) {
1396: if ($type->isSuperTypeOf($receivedInnerType)->yes()) {
1397: $types = $types->union($type->inferTemplateTypes($receivedInnerType));
1398: continue 2;
1399: }
1400: }
1401: $remainingReceivedTypes[] = $receivedInnerType;
1402: }
1403: if (count($remainingReceivedTypes) === 0) {
1404: return $types->union($this->getAbsorbedTemplateTypes($types));
1405: }
1406: if (count($remainingReceivedTypes) !== count($receivedTypes)) {
1407: $receivedType = TypeCombinator::union(...$remainingReceivedTypes);
1408: }
1409:
1410: foreach ($this->types as $type) {
1411: if (self::getNakedTemplateType($type) !== null) {
1412: continue;
1413: }
1414: $types = $types->union($type->inferTemplateTypes($receivedType));
1415: }
1416:
1417: if (!$types->isEmpty()) {
1418: return $types;
1419: }
1420:
1421: foreach ($this->types as $type) {
1422: $types = $types->union($type->inferTemplateTypes($receivedType));
1423: }
1424:
1425: return $types;
1426: }
1427:
1428: private static function getNakedTemplateType(Type $type): ?TemplateType
1429: {
1430: if ($type instanceof TemplateType) {
1431: return $type;
1432: }
1433:
1434: if ($type instanceof GenericClassStringType) {
1435: $genericType = $type->getGenericType();
1436: if ($genericType instanceof TemplateType) {
1437: return $genericType;
1438: }
1439: }
1440:
1441: return null;
1442: }
1443:
1444: /**
1445: * Naked template members that the absorption above left without a type were not
1446: * forgotten - the parameter type declares them next to a member that took the
1447: * whole argument, so there was nothing for them to infer from.
1448: */
1449: private function getAbsorbedTemplateTypes(TemplateTypeMap $inferred): TemplateTypeMap
1450: {
1451: $absorbed = [];
1452: foreach ($this->types as $type) {
1453: $templateType = self::getNakedTemplateType($type);
1454: if ($templateType === null) {
1455: continue;
1456: }
1457: if ($inferred->hasType($templateType->getName())) {
1458: continue;
1459: }
1460:
1461: $absorbed[$templateType->getName()] = new AbsorbedTemplateArgumentType();
1462: }
1463:
1464: if (count($absorbed) === 0) {
1465: return TemplateTypeMap::createEmpty();
1466: }
1467:
1468: return new TemplateTypeMap($absorbed);
1469: }
1470:
1471: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
1472: {
1473: $types = TemplateTypeMap::createEmpty();
1474:
1475: foreach ($this->types as $type) {
1476: $types = $types->union($templateType->inferTemplateTypes($type));
1477: }
1478:
1479: return $types;
1480: }
1481:
1482: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1483: {
1484: $references = [];
1485:
1486: foreach ($this->types as $type) {
1487: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
1488: $references[] = $reference;
1489: }
1490: }
1491:
1492: return $references;
1493: }
1494:
1495: public function traverse(callable $cb): Type
1496: {
1497: $types = [];
1498: $changed = false;
1499:
1500: foreach ($this->types as $type) {
1501: $newType = $cb($type);
1502: if ($type !== $newType) {
1503: $changed = true;
1504: }
1505: $types[] = $newType;
1506: }
1507:
1508: if ($changed) {
1509: return TypeCombinator::union(...$types);
1510: }
1511:
1512: return $this;
1513: }
1514:
1515: public function traverseSimultaneously(Type $right, callable $cb): Type
1516: {
1517: $rightTypes = TypeUtils::flattenTypes($right);
1518: $newTypes = [];
1519: $changed = false;
1520: foreach ($this->types as $innerType) {
1521: $candidates = [];
1522: foreach ($rightTypes as $i => $rightType) {
1523: if (!$innerType->isSuperTypeOf($rightType)->yes()) {
1524: continue;
1525: }
1526:
1527: $candidates[] = $rightType;
1528: unset($rightTypes[$i]);
1529: }
1530:
1531: if (count($candidates) === 0) {
1532: $newTypes[] = $innerType;
1533: continue;
1534: }
1535:
1536: $newType = $cb($innerType, TypeCombinator::union(...$candidates));
1537: if ($innerType !== $newType) {
1538: $changed = true;
1539: }
1540:
1541: $newTypes[] = $newType;
1542: }
1543:
1544: if ($changed) {
1545: return TypeCombinator::union(...$newTypes);
1546: }
1547:
1548: return $this;
1549: }
1550:
1551: public function tryRemove(Type $typeToRemove): ?Type
1552: {
1553: $finiteTypeSet = $this->getFiniteTypeSet();
1554: if ($finiteTypeSet !== null && $finiteTypeSet->isComplete()) {
1555: $key = FiniteTypeSet::key($typeToRemove);
1556: if ($key !== null) {
1557: if (!$finiteTypeSet->has($key)) {
1558: return null;
1559: }
1560:
1561: $remainingTypes = [];
1562: foreach ($finiteTypeSet->getMembers() as $memberKey => $member) {
1563: if ($memberKey === $key) {
1564: continue;
1565: }
1566:
1567: $remainingTypes[] = $member;
1568: }
1569:
1570: if (count($remainingTypes) === 1) {
1571: return $remainingTypes[0];
1572: }
1573:
1574: return new UnionType($remainingTypes);
1575: }
1576:
1577: // Every member of this union is a distinct value, and so is every member
1578: // of the removed one: the result is their set difference, which no order
1579: // of removals can sharpen. Taking it in one pass keeps removing a union
1580: // of values linear instead of re-deriving this union once per value.
1581: if ($typeToRemove instanceof self) {
1582: $keysToRemove = [];
1583: foreach ($typeToRemove->getTypes() as $innerTypeToRemove) {
1584: $innerKey = FiniteTypeSet::key($innerTypeToRemove);
1585: if ($innerKey === null) {
1586: $keysToRemove = null;
1587: break;
1588: }
1589:
1590: $keysToRemove[$innerKey] = true;
1591: }
1592:
1593: if ($keysToRemove !== null) {
1594: $remainingTypes = [];
1595: $removedAny = false;
1596: foreach ($finiteTypeSet->getMembers() as $memberKey => $member) {
1597: if (array_key_exists($memberKey, $keysToRemove)) {
1598: $removedAny = true;
1599: continue;
1600: }
1601:
1602: $remainingTypes[] = $member;
1603: }
1604:
1605: if (!$removedAny) {
1606: return null;
1607: }
1608:
1609: if (count($remainingTypes) === 0) {
1610: return new NeverType();
1611: }
1612:
1613: if (count($remainingTypes) === 1) {
1614: return $remainingTypes[0];
1615: }
1616:
1617: return new UnionType($remainingTypes);
1618: }
1619: }
1620: }
1621:
1622: $innerTypes = [];
1623: $changed = false;
1624: foreach ($this->types as $innerType) {
1625: $removed = TypeCombinator::remove($innerType, $typeToRemove);
1626: if (!$removed->equals($innerType)) {
1627: $changed = true;
1628: }
1629: if ($removed instanceof NeverType) {
1630: continue;
1631: }
1632: if ($removed instanceof self && !$removed instanceof TemplateType) {
1633: foreach ($removed->getTypes() as $removedInnerType) {
1634: $innerTypes[] = $removedInnerType;
1635: }
1636: } else {
1637: $innerTypes[] = $removed;
1638: }
1639: }
1640:
1641: if (!$changed) {
1642: return null;
1643: }
1644:
1645: if (count($innerTypes) === 0) {
1646: return new NeverType();
1647: }
1648:
1649: if (count($innerTypes) === 1) {
1650: return $innerTypes[0];
1651: }
1652:
1653: return new UnionType($innerTypes);
1654: }
1655:
1656: public function exponentiate(Type $exponent): Type
1657: {
1658: return $this->unionTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1659: }
1660:
1661: public function getFiniteTypes(): array
1662: {
1663: if ($this->finiteTypes !== null) {
1664: return $this->finiteTypes;
1665: }
1666:
1667: $types = $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getFiniteTypes());
1668: $uniquedTypes = [];
1669: foreach ($types as $type) {
1670: // These are finite types, so FiniteTypeSet keys them by the value they
1671: // stand for - which is what the uniquing wants and what describe() was
1672: // standing in for. Floats have no such key (equals() does not agree with
1673: // value identity for them) and keep describing themselves.
1674: $uniquedTypes[FiniteTypeSet::key($type) ?? $type->describe(VerbosityLevel::cache())] = $type;
1675: }
1676:
1677: if (count($uniquedTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1678: return $this->finiteTypes = [];
1679: }
1680:
1681: return $this->finiteTypes = array_values($uniquedTypes);
1682: }
1683:
1684: /**
1685: * @param callable(Type $type): TrinaryLogic $getResult
1686: */
1687: protected function unionResults(callable $getResult): TrinaryLogic
1688: {
1689: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1690: }
1691:
1692: /**
1693: * @param callable(Type $type): TrinaryLogic $getResult
1694: */
1695: private function notBenevolentUnionResults(callable $getResult): TrinaryLogic
1696: {
1697: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1698: }
1699:
1700: /**
1701: * @param callable(Type $type): Type $getType
1702: */
1703: protected function unionTypes(callable $getType): Type
1704: {
1705: $newTypes = [];
1706: $changed = false;
1707: foreach ($this->types as $type) {
1708: $newType = $getType($type);
1709: if ($newType !== $type) {
1710: $changed = true;
1711: }
1712: $newTypes[] = $newType;
1713: }
1714:
1715: if (!$changed) {
1716: return $this;
1717: }
1718:
1719: return TypeCombinator::union(...$newTypes);
1720: }
1721:
1722: /**
1723: * @template T
1724: * @param callable(Type $type): list<T> $getValues
1725: * @param callable(Type $type): bool $criteria
1726: * @return list<T>
1727: */
1728: protected function pickFromTypes(
1729: callable $getValues,
1730: callable $criteria,
1731: ): array
1732: {
1733: $values = [];
1734: foreach ($this->types as $type) {
1735: $innerValues = $getValues($type);
1736: if ($innerValues === []) {
1737: return [];
1738: }
1739:
1740: foreach ($innerValues as $innerType) {
1741: $values[] = $innerType;
1742: }
1743: }
1744:
1745: return $values;
1746: }
1747:
1748: public function toPhpDocNode(): TypeNode
1749: {
1750: return new UnionTypeNode(array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->getSortedTypes()));
1751: }
1752:
1753: /**
1754: * @template T
1755: * @param callable(Type $type): list<T> $getValues
1756: * @return list<T>
1757: */
1758: private function notBenevolentPickFromTypes(callable $getValues): array
1759: {
1760: $values = [];
1761: foreach ($this->types as $type) {
1762: $innerValues = $getValues($type);
1763: if ($innerValues === []) {
1764: return [];
1765: }
1766:
1767: foreach ($innerValues as $innerType) {
1768: $values[] = $innerType;
1769: }
1770: }
1771:
1772: return $values;
1773: }
1774:
1775: public function hasTemplateOrLateResolvableType(): bool
1776: {
1777: foreach ($this->types as $type) {
1778: if (!$type->hasTemplateOrLateResolvableType()) {
1779: continue;
1780: }
1781:
1782: return true;
1783: }
1784:
1785: return false;
1786: }
1787:
1788: }
1789: