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