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