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